# Batch 14 Controllers Analysis (Controllers 131-140) ## Controllers in this batch: 1. RankController 2. RelationshipController 3. ReligionController 4. ReportController (partial - file too large, analyzed first 100 lines) 5. ScriptProfileOrgController 6. SocketController 7. SubDistrictController 8. UserController 9. ViewWorkFlowController 10. WorkflowController --- ## Critical Issues Found ### 1. **UserController** - Multiple Unhandled forEach Async Operations **File & Location:** [UserController.ts](src/controllers/UserController.ts) - Methods: `createUserImport()`, `addroleStaffToUser()`, `addroleStaffToUserEmp()`, `changeUserPasswordAll()` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** Multiple critical methods use `for await` loops and `forEach()` with async Keycloak API operations without proper error handling. When Keycloak operations fail, the errors can crash the Node.js process. **Affected Code Locations:** - Line 977-1032: `for await` loop in `createUserImport()` - No error handling for individual user creation failures - Line 1133-1148: `for await` loop in `changeUserPasswordAll()` - Errors are silently ignored but not properly handled - Line 1169-1227: `for await` loop in `addroleStaffToUser()` - Keycloak operations without error handling - Line 1249-1307: `for await` loop in `addroleStaffToUserEmp()` - Keycloak operations without error handling - Line 1066-1118: `Promise.all()` in `createUserImportEmp()` - Limited error handling **Code Examples:** ```typescript // Line 977-1032 - DANGEROUS: for await without error handling for await (const _item of profiles) { let password = _item.citizenId; if (_item.birthDate != null) { const _date = new Date(_item.birthDate.toDateString()) .getDate() .toString() .padStart(2, "0"); const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1) .toString() .padStart(2, "0"); const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543; password = `${_date}${_month}${_year}`; } const checkUser = await getUserByUsername(_item.citizenId); let userId: any = ""; if (checkUser.length == 0) { userId = await createUser(_item.citizenId, password, { firstName: _item.firstName, lastName: _item.lastName, }); if (typeof userId !== "string") { throw new Error(userId.errorMessage); // This can crash the entire process } } else { userId = checkUser[0].id; } const list = await getRoles(); if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server."); const result = await addUserRoles( userId, list.filter((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"), ); if (!result) { throw new Error("Failed. Cannot set user's role."); // This can crash the entire process } // ... more operations without error handling } ``` ```typescript // Line 1066-1118 - Promise.all with some error handling but not comprehensive await Promise.all( batch.map(async (_item) => { // ... operations try { const checkUser = await getUserByUsername(_item.citizenId); // ... more operations } catch (error) { console.error(`Error processing ${_item.citizenId}:`, error); } }), ); ``` **Recommended Fix:** ```typescript // For createUserImport() - Add comprehensive error handling @Post("user/create") @Security("bearerAuth", ["system", "admin"]) async createUserImport( @Request() request: { user: { sub: string; preferred_username: string } }, ) { const profiles = await this.profileRepo.find({ where: { keycloak: IsNull(), }, relations: ["roleKeycloaks"], }); const results = { total: profiles.length, success: 0, failed: 0, errors: [] as Array<{ citizenId: string; error: string }>, }; // Cache roles list to avoid repeated API calls let rolesList: any[] = []; try { rolesList = await getRoles(); if (!Array.isArray(rolesList)) { throw new Error("Failed. Cannot get role(s) data from the server."); } } catch (error) { console.error('Failed to fetch roles:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to fetch roles from Keycloak' ); } const defaultRole = rolesList.find((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"); if (!defaultRole) { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Default role not found in Keycloak' ); } for await (const _item of profiles) { try { let password = _item.citizenId; if (_item.birthDate != null) { const _date = new Date(_item.birthDate.toDateString()) .getDate() .toString() .padStart(2, "0"); const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1) .toString() .padStart(2, "0"); const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543; password = `${_date}${_month}${_year}`; } const checkUser = await getUserByUsername(_item.citizenId); let userId: string = ""; if (checkUser.length == 0) { const createdUser = await createUser(_item.citizenId, password, { firstName: _item.firstName, lastName: _item.lastName, }); if (typeof createdUser !== "string") { throw new Error(createdUser.errorMessage || 'Failed to create user'); } userId = createdUser; } else { userId = checkUser[0].id; } const result = await addUserRoles(userId, [defaultRole]); if (!result) { throw new Error("Failed. Cannot set user's role."); } if (typeof userId === "string") { _item.keycloak = userId; } const roleKeycloak = await this.roleKeycloakRepo.find({ where: { id: "8a1a0dc9-304c-4e5b-a90a-65f841048212" }, }); if (_item) { _item.roleKeycloaks = Array.from(new Set([..._item.roleKeycloaks, ...roleKeycloak])); await this.profileRepo.save(_item); } results.success++; } catch (error: any) { results.failed++; results.errors.push({ citizenId: _item.citizenId, error: error.message || 'Unknown error', }); console.error(`Error processing user ${_item.citizenId}:`, error); } } return new HttpSuccess({ message: 'User import completed', ...results, }); } // For addroleStaffToUser() - Add error handling with detailed logging @Post("add-role-staff/user/{child1Id}") @Security("bearerAuth", ["system", "admin"]) async addroleStaffToUser( @Path() child1Id: string, @Request() request: { user: { sub: string; preferred_username: string } }, ) { const profiles = await this.profileRepo.find({ where: { keycloak: Not(IsNull()), current_holders: { orgChild1Id: child1Id, }, }, relations: ["roleKeycloaks"], }); const results = { total: profiles.length, success: 0, failed: 0, errors: [] as Array<{ citizenId: string; error: string }>, }; // Cache roles let rolesList: any[] = []; try { rolesList = await getRoles(); if (!Array.isArray(rolesList)) { throw new Error("Failed. Cannot get role(s) data from the server."); } } catch (error) { console.error('Failed to fetch roles:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to fetch roles from Keycloak' ); } const userRole = rolesList.find((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"); const staffRole = rolesList.find((v) => v.id == "f1fff8db-0795-47c1-9952-f3c18d5b6172"); if (!userRole || !staffRole) { throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Required roles not found in Keycloak' ); } for await (const _item of profiles) { try { let password = _item.citizenId; if (_item.birthDate != null) { const _date = new Date(_item.birthDate.toDateString()) .getDate() .toString() .padStart(2, "0"); const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1) .toString() .padStart(2, "0"); const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543; password = `${_date}${_month}${_year}`; } const checkUser = await getUserByUsername(_item.citizenId); let userId: string = ""; if (checkUser.length == 0) { const createdUser = await createUser(_item.citizenId, password, { firstName: _item.firstName, lastName: _item.lastName, }); if (typeof createdUser !== "string") { throw new Error(createdUser.errorMessage || 'Failed to create user'); } userId = createdUser; } else { userId = checkUser[0].id; } // Add both roles await Promise.all([ addUserRoles(userId, [userRole]), addUserRoles(userId, [staffRole]), ]); if (typeof userId === "string") { _item.keycloak = userId; } const roleKeycloakUser = await this.roleKeycloakRepo.find({ where: { id: "8a1a0dc9-304c-4e5b-a90a-65f841048212" }, }); const roleKeycloakStaff = await this.roleKeycloakRepo.find({ where: { id: "f1fff8db-0795-47c1-9952-f3c18d5b6172" }, }); if (_item) { _item.roleKeycloaks = Array.from(new Set([...roleKeycloakUser, ...roleKeycloakStaff])); await this.profileRepo.save(_item); } results.success++; } catch (error: any) { results.failed++; results.errors.push({ citizenId: _item.citizenId, error: error.message || 'Unknown error', }); console.error(`Error processing user ${_item.citizenId}:`, error); } } return new HttpSuccess({ message: 'Role assignment completed', ...results, }); } // For changeUserPasswordAll() - Add proper error handling @Post("user/change-password-all") async changeUserPasswordAll( @Request() request: { user: { sub: string; preferred_username: string } }, ) { const profiles = await this.profileRepo.find({ where: { keycloak: Not(IsNull()), }, }); const results = { total: profiles.length, success: 0, failed: 0, errors: [] as Array<{ citizenId: string; error: string }>, }; for await (const _item of profiles) { try { let password = _item.citizenId; if (_item.birthDate != null) { const gregorianYear = _item.birthDate.getFullYear() + 543; const formattedDate = _item.birthDate.toISOString().slice(8, 10) + _item.birthDate.toISOString().slice(5, 7) + gregorianYear; password = formattedDate; } const result = await changeUserPassword(_item.keycloak, password); if (!result) { throw new Error('Failed to change password'); } results.success++; } catch (error: any) { results.failed++; results.errors.push({ citizenId: _item.citizenId, error: error.message || 'Unknown error', }); console.error(`Error changing password for ${_item.citizenId}:`, error); } } return new HttpSuccess({ message: 'Password change completed', ...results, }); } ``` --- ### 2. **WorkflowController** - Multiple Database Operations Without Transactions **File & Location:** [WorkflowController.ts](src/controllers/WorkflowController.ts) - Method: `checkWorkflow()` **Problem Type:** 2. Missing Error Handle **Root Cause:** Complex multi-step workflow creation process without transactional integrity. If intermediate operations fail, the database can be left in an inconsistent state with partial data. **Affected Code Locations:** - Line 46-273: `checkWorkflow()` - Multiple sequential database operations without transaction - Line 143-180: `forEach()` with state operator creation - No error handling for individual operations - Line 214-230: `forEach()` for officer state operator users - No error handling - Line 258-270: Fire-and-forget API call with only console error logging **Code Examples:** ```typescript // Line 111-133 - Multiple database operations without transaction const [savedWorkflow, metaStates] = await Promise.all([ this.workflowRepo.save(workflow), this.metaStateRepo.find({ where: { metaWorkflowId: metaWorkflow.id }, order: { order: "ASC" }, }), ]); // ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว const statesToCreate = metaStates.map((item) => { const state = new State(); Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta }); return state; }); const savedStates = await this.stateRepo.save(statesToCreate); // ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก const firstState = savedStates.find((state) => state.order === 1); if (firstState) { savedWorkflow.stateId = firstState.id; await this.workflowRepo.save(savedWorkflow); } ``` ```typescript // Line 214-230 - forEach without error handling profileOfficers.forEach((item) => { if (item.current_holderId) { orderNum += 1; const isPersonnelOfficer = item.orgChild1?.isOfficer === true; const officerStateOperatorUser = new StateOperatorUser(); Object.assign(officerStateOperatorUser, { profileId: item.current_holderId, operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer", profileType: "OFFICER", order: orderNum, workflowId: savedWorkflow.id, ...meta, }); stateOperatorUsersToCreate.push(officerStateOperatorUser); } }); ``` ```typescript // Line 258-270 - Fire-and-forget API call new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, receiverUserIds: notificationReceivers, payload: "", isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling API:", error); }); ``` **Recommended Fix:** ```typescript @Post("add-workflow") public async checkWorkflow( @Request() req: RequestWithUser, @Body() body: { refId: string; sysName: string; posLevelName: string; posTypeName: string; fullName?: string | null; isDeputy?: boolean | null; orgRootId?: string | null; }, ) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // ขั้นที่ 1: ทำการค้นหา profile และ metaWorkflow แบบ parallel const [userProfileOfficer, userProfileEmployee, metaWorkflow] = await Promise.all([ queryRunner.manager.findOne(Profile, { where: { keycloak: req.user.sub }, select: ["id", "keycloak"], }), queryRunner.manager.findOne(ProfileEmployee, { where: { keycloak: req.user.sub }, select: ["id", "keycloak"], }), queryRunner.manager.findOne(MetaWorkflow, { where: { sysName: body.sysName, posLevelName: body.posLevelName, posTypeName: body.posTypeName, }, }), ]); // กำหนด profile type และ profile let profileType = "OFFICER"; let profile: any = userProfileOfficer; if (!profile) { profileType = "EMPLOYEE"; profile = userProfileEmployee; if (!profile) { await queryRunner.rollbackTransaction(); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน"); } } if (!metaWorkflow) { await queryRunner.rollbackTransaction(); throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบกระบวนการนี้ได้"); } const meta = { createdUserId: req.user.sub, createdFullName: req.user.name, lastUpdateUserId: req.user.sub, lastUpdateFullName: req.user.name, createdAt: new Date(), lastUpdatedAt: new Date(), }; // ขั้นที่ 2: สร้าง workflow และดึง metaState แบบ parallel const workflow = new Workflow(); Object.assign(workflow, { ...metaWorkflow, id: undefined, ...meta, ...body, profileType: profileType, system: body.sysName, }); const savedWorkflow = await queryRunner.manager.save(workflow); const metaStates = await queryRunner.manager.find(MetaState, { where: { metaWorkflowId: metaWorkflow.id }, order: { order: "ASC" }, }); // ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว const statesToCreate = metaStates.map((item) => { const state = new State(); Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta }); return state; }); const savedStates = await queryRunner.manager.save(statesToCreate); // ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก const firstState = savedStates.find((state) => state.order === 1); if (firstState) { savedWorkflow.stateId = firstState.id; await queryRunner.manager.save(savedWorkflow); } // ขั้นที่ 5: ดึง metaStateOperators ทั้งหมดและสร้าง stateOperators const metaStateIds = metaStates.map((item) => item.id); const allMetaStateOperators = await queryRunner.manager.find(MetaStateOperator, { where: { metaStateId: In(metaStateIds) }, }); // สร้าง stateOperators ทั้งหมดในครั้งเดียว const stateOperatorsToCreate: StateOperator[] = []; allMetaStateOperators.forEach((metaStateOp) => { const correspondingState = savedStates.find( (state) => metaStates.find((metaState) => metaState.id === metaStateOp.metaStateId)?.order === state.order, ); if (body.isDeputy) { // Task #2207 กรณีคนขอโอนอยู่ในสำนักปลัดกรุงเทพมหานคร if (body.sysName == "SYS_TRANSFER_REQ") { if (metaStateOp.operator == "PersonnelOfficer" && correspondingState?.order == 1) { return; } else if ( metaStateOp.operator == "Officer" && [1, 2].includes(correspondingState?.order as number) ) { metaStateOp.operator = "PersonnelOfficer"; } } // Task #2208 กรณีขอแก้ไขข้อมูลทะเบียนประวัติ และ IDP และคนขออยู่ในสำนักปลัดกรุงเทพมหานคร if ( metaStateOp.operator == "Officer" && ["REGISTRY_PROFILE", "REGISTRY_PROFILE_EMP", "REGISTRY_IDP"].includes(body.sysName) ) { metaStateOp.operator = "PersonnelOfficer"; } } if (correspondingState) { const stateOperator = new StateOperator(); Object.assign(stateOperator, { ...metaStateOp, id: undefined, stateId: correspondingState.id, ...meta, }); stateOperatorsToCreate.push(stateOperator); } }); await queryRunner.manager.save(stateOperatorsToCreate); // ขั้นที่ 6: สร้าง StateOperatorUsers แบบ bulk const stateOperatorUsersToCreate: StateOperatorUser[] = []; let orderNum = 1; // เพิ่ม Owner ก่อน if (profile) { const ownerStateOperatorUser = new StateOperatorUser(); Object.assign(ownerStateOperatorUser, { profileId: profileType === "OFFICER" ? profile.id : null, profileEmployeeId: profileType !== "OFFICER" ? profile.id : null, profileType: profileType, operator: "Owner", order: orderNum, workflowId: savedWorkflow.id, ...meta, }); stateOperatorUsersToCreate.push(ownerStateOperatorUser); } // ดึงข้อมูล profileOfficers และสร้าง StateOperatorUsers const profileOfficers = await queryRunner.manager.find(PosMaster, { where: { posMasterAssigns: { assignId: body.sysName }, orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true }, current_holderId: Not(IsNull()), ...(body.orgRootId && { orgRootId: body.orgRootId }), }, relations: ["orgChild1"], }); // สร้าง StateOperatorUsers สำหรับ officers - with error handling for (const item of profileOfficers) { try { if (item.current_holderId) { orderNum += 1; const isPersonnelOfficer = item.orgChild1?.isOfficer === true; const officerStateOperatorUser = new StateOperatorUser(); Object.assign(officerStateOperatorUser, { profileId: item.current_holderId, operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer", profileType: "OFFICER", order: orderNum, workflowId: savedWorkflow.id, ...meta, }); stateOperatorUsersToCreate.push(officerStateOperatorUser); } } catch (error) { console.error(`Error processing officer ${item.current_holderId}:`, error); // Continue with next officer } } // บันทึก StateOperatorUsers ทั้งหมดในครั้งเดียว await queryRunner.manager.save(stateOperatorUsersToCreate); // Commit transaction await queryRunner.commitTransaction(); // ขั้นที่ 7: ส่ง notification (fire-and-forget after transaction commits) const firstStateOperators = stateOperatorsToCreate.filter((so) => savedStates.find((state) => state.id === so.stateId && state.order === 1), ); let notiLink = ""; if (body.sysName === "REGISTRY_PROFILE") { notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`; } else if (body.sysName === "REGISTRY_PROFILE_EMP") { notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`; } else if (body.sysName === "REGISTRY_IDP") { notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`; } const notificationReceivers = stateOperatorUsersToCreate .filter((user) => firstStateOperators.some((op) => op.operator === user.operator)) .map((user) => ({ receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId, notiLink: notiLink, })); // Send notification asynchronously with proper error handling new CallAPI() .PostData(req, "/placement/noti/profiles", { subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`, receiverUserIds: notificationReceivers, payload: "", isSendMail: true, isSendInbox: true, isSendNotification: true, }) .catch((error) => { console.error("Error calling notification API:", { workflowId: savedWorkflow.id, error: error instanceof Error ? error.message : error, }); }); return new HttpSuccess(); } catch (error) { await queryRunner.rollbackTransaction(); console.error('Error in checkWorkflow:', { refId: body.refId, sysName: body.sysName, error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined, }); if (error instanceof HttpError) { throw error; } throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to create workflow' ); } finally { await queryRunner.release(); } } ``` --- ### 3. **ScriptProfileOrgController** - Missing Error Handling for External API Calls **File & Location:** [ScriptProfileOrgController.ts](src/controllers/ScriptProfileOrgController.ts) - Method: `cronjobUpdateOrg()` **Problem Type:** 2. Missing Error Handle **Root Cause:** While this controller has good error handling structure, the external API call to the leave service and Keycloak sync operations need better error handling for individual batch failures. **Affected Code Locations:** - Line 184-190: External API call without detailed error handling - Line 228-250: Batch processing with limited error context - Line 71-159: Complex database queries without error handling **Code Examples:** ```typescript // Line 184-190 - External API call needs better error handling await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { headers: { "Content-Type": "application/json", api_key: process.env.API_KEY, }, timeout: 30000, }); ``` ```typescript // Line 228-250 - Batch processing could use more error context try { const batchResult: any = await keycloakSyncController.syncByProfileIds({ profileIds: batch, profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", }); const resultData = (batchResult as any)?.data || batchResult; typeResult.success += resultData.success || 0; typeResult.failed += resultData.failed || 0; console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { success: resultData.success || 0, failed: resultData.failed || 0, }); } catch (error: any) { console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { error: error.message, batchSize: batch.length, }); typeResult.failed += batch.length; } ``` **Recommended Fix:** ```typescript // Improve external API call error handling try { const response = await axios.put( `${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, { headers: { "Content-Type": "application/json", api_key: process.env.API_KEY, }, timeout: 30000, } ); console.log("cronjobUpdateOrg: Leave service API call successful", { status: response.status, payloadCount: payloads.length, }); } catch (error: any) { console.error("cronjobUpdateOrg: Leave service API call failed", { error: error.message, response: error.response?.data, status: error.response?.status, payloadCount: payloads.length, }); // Don't fail completely - log and continue // Optionally: implement retry logic or circuit breaker } // Improve batch processing error handling for (let i = 0; i < batches.length; i++) { const batch = batches[i]; console.log( `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`, { batchSize: batch.length, batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min( (i + 1) * this.BATCH_SIZE, profileIds.length, )}`, }, ); try { const batchResult: any = await keycloakSyncController.syncByProfileIds({ profileIds: batch, profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE", }); const resultData = (batchResult as any)?.data || batchResult; typeResult.success += resultData.success || 0; typeResult.failed += resultData.failed || 0; console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, { success: resultData.success || 0, failed: resultData.failed || 0, }); } catch (error: any) { console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, { error: error.message, stack: error.stack, batchSize: batch.length, batchIndex: i, profileType: profileType, }); // Count all profiles in failed batch as failed typeResult.failed += batch.length; // Optionally: Store failed batch for retry // failedBatches.push({ index: i, batch, error: error.message }); } } ``` --- ### 4. **SubDistrictController** - Generic Error Handling Without Logging **File & Location:** [SubDistrictController.ts](src/controllers/SubDistrictController.ts) - Method: `Delete()` **Problem Type:** 2. Missing Error Handle **Root Cause:** The delete operation uses a generic catch block without logging or differentiating between error types. This makes debugging difficult and may mask underlying issues. **Affected Code Locations:** - Line 183-190: Generic catch block without error logging **Code Example:** ```typescript // Line 183-190 - Generic error handling let result: any; try { result = await this.subDistrictRepository.delete({ id: id }); } catch { throw new HttpError( HttpStatusCode.NOT_FOUND, "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลแขวง/ตำบลนี้อยู่", ); } ``` **Recommended Fix:** ```typescript let result: any; try { result = await this.subDistrictRepository.delete({ id: id }); } catch (error: any) { console.error('Error deleting sub-district:', { id, error: error.message, stack: error.stack, code: error.code, }); // Check for foreign key constraint error if (error.code === 'ER_ROW_IS_REFERENCED_2' || error.message?.includes('foreign key constraint') || error.message?.includes('Cannot delete or update a parent row')) { throw new HttpError( HttpStatusCode.CONFLICT, "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลแขวง/ตำบลนี้อยู่", ); } throw new HttpError( HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการลบข้อมูลแขวง/ตำบล", ); } ``` --- ### 5. **UserController** - Promise.all Without Comprehensive Error Handling **File & Location:** [UserController.ts](src/controllers/UserController.ts) - Method: `listUserKeycloak()` **Problem Type:** 2. Missing Error Handle **Root Cause:** Complex database query with multiple conditions and joins without proper error handling. If the query fails or the database is unavailable, the error will propagate unhandled. **Affected Code Locations:** - Line 566-608: Complex query builder operations for OFFICER type - Line 610-653: Complex query builder operations for EMPLOYEE type **Code Example:** ```typescript // Line 566-608 - Complex query without error handling [profiles, total] = await this.profileRepo .createQueryBuilder("profile") .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloaks") .leftJoinAndSelect("profile.current_holders", "current_holders") .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") .andWhere("profile.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere( new Brackets((qb) => { qb.orWhere( body.keyword != null && body.keyword != "" ? `profile.citizenId like '%${body.keyword}%'` : "1=1", ) .orWhere( body.keyword != null && body.keyword != "" ? `profile.email like '%${body.keyword}%'` : "1=1", ) .orWhere( body.keyword != null && body.keyword != "" ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like '%${body.keyword}%'` : "1=1", ); }), ) .orderBy("profile.citizenId", "ASC") .orderBy("orgRoot.orgRootOrder", "ASC") .addOrderBy("orgChild1.orgChild1Order", "ASC") .addOrderBy("orgChild2.orgChild2Order", "ASC") .addOrderBy("orgChild3.orgChild3Order", "ASC") .addOrderBy("orgChild4.orgChild4Order", "ASC") .addOrderBy("current_holders.posMasterOrder", "ASC") .addOrderBy("current_holders.posMasterCreatedAt", "ASC") .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getManyAndCount(); ``` **Recommended Fix:** ```typescript let profiles: any = []; let total: any; try { if (body.type.trim().toUpperCase() == "OFFICER") { try { [profiles, total] = await this.profileRepo .createQueryBuilder("profile") .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloaks") .leftJoinAndSelect("profile.current_holders", "current_holders") .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''") .andWhere("profile.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere( new Brackets((qb) => { qb.orWhere( body.keyword != null && body.keyword != "" ? `profile.citizenId like :citizenKeyword` : "1=1", { citizenKeyword: body.keyword ? `%${body.keyword}%` : '' }, ) .orWhere( body.keyword != null && body.keyword != "" ? `profile.email like :emailKeyword` : "1=1", { emailKeyword: body.keyword ? `%${body.keyword}%` : '' }, ) .orWhere( body.keyword != null && body.keyword != "" ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like :nameKeyword` : "1=1", { nameKeyword: body.keyword ? `%${body.keyword}%` : '' }, ); }), ) .orderBy("profile.citizenId", "ASC") .addOrderBy("orgRoot.orgRootOrder", "ASC") .addOrderBy("orgChild1.orgChild1Order", "ASC") .addOrderBy("orgChild2.orgChild2Order", "ASC") .addOrderBy("orgChild3.orgChild3Order", "ASC") .addOrderBy("orgChild4.orgChild4Order", "ASC") .addOrderBy("current_holders.posMasterOrder", "ASC") .addOrderBy("current_holders.posMasterCreatedAt", "ASC") .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getManyAndCount(); } catch (error: any) { console.error('Error querying officer profiles:', { error: error.message, stack: error.stack, body: { ...body, keyword: body.keyword ? '[REDACTED]' : null }, }); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to retrieve officer profiles' ); } } else if (body.type.trim().toUpperCase() == "EMPLOYEE") { try { [profiles, total] = await this.profileEmpRepo .createQueryBuilder("profileEmployee") .leftJoinAndSelect("profileEmployee.roleKeycloaks", "roleKeycloaks") .leftJoinAndSelect("profileEmployee.current_holders", "current_holders") .leftJoinAndSelect("current_holders.orgRoot", "orgRoot") .leftJoinAndSelect("current_holders.orgChild1", "orgChild1") .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''") .andWhere("profileEmployee.isDelete = :isDelete", { isDelete: false }) .andWhere(checkChildFromRole) .andWhere(conditions) .andWhere({ employeeClass: "PERM" }) .andWhere( new Brackets((qb) => { qb.orWhere( body.keyword != null && body.keyword != "" ? `profileEmployee.citizenId like :citizenKeyword` : "1=1", { citizenKeyword: body.keyword ? `%${body.keyword}%` : '' }, ) .orWhere( body.keyword != null && body.keyword != "" ? `profileEmployee.email like :emailKeyword` : "1=1", { emailKeyword: body.keyword ? `%${body.keyword}%` : '' }, ) .orWhere( body.keyword != null && body.keyword != "" ? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) like :nameKeyword` : "1=1", { nameKeyword: body.keyword ? `%${body.keyword}%` : '' }, ); }), ) .orderBy("profileEmployee.citizenId", "ASC") .addOrderBy("orgRoot.orgRootOrder", "ASC") .addOrderBy("orgChild1.orgChild1Order", "ASC") .addOrderBy("orgChild2.orgChild2Order", "ASC") .addOrderBy("orgChild3.orgChild3Order", "ASC") .addOrderBy("orgChild4.orgChild4Order", "ASC") .addOrderBy("current_holders.posMasterOrder", "ASC") .addOrderBy("current_holders.posMasterCreatedAt", "ASC") .skip((body.page - 1) * body.pageSize) .take(body.pageSize) .getManyAndCount(); } catch (error: any) { console.error('Error querying employee profiles:', { error: error.message, stack: error.stack, body: { ...body, keyword: body.keyword ? '[REDACTED]' : null }, }); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to retrieve employee profiles' ); } } } catch (error) { if (error instanceof HttpError) { throw error; } throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to retrieve user data' ); } const _profiles = profiles.map((_data: any) => ({ id: _data.keycloak, firstname: _data.firstName, lastname: _data.lastName, email: _data.email, username: _data.citizenId, citizenId: _data.citizenId, roles: _data.roleKeycloaks, enabled: _data.isActive, })); return new HttpSuccess({ data: _profiles, total }); ``` --- ### 6. **SocketController** - No Error Handling for WebSocket Operations **File & Location:** [SocketController.ts](src/controllers/SocketController.ts) - Method: `notify()` **Problem Type:** 2. Missing Error Handle **Root Cause:** The WebSocket send operation has no error handling. If the WebSocket service fails, the error will be unhandled. **Affected Code Locations:** - Line 7-24: Entire notify method **Code Example:** ```typescript @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 || [], }, ); } ``` **Recommended Fix:** ```typescript @Post("notify") async notify( @Body() payload: { message: string; userId?: string | string[]; roles?: string | string[]; error?: boolean; }, ) { try { sendWebSocket( "socket-notification", { success: !payload.error, message: payload.message }, { roles: payload.roles || [], userId: payload.userId || [], }, ); return new HttpSuccess({ message: 'Notification sent successfully' }); } catch (error: any) { console.error('Error sending WebSocket notification:', { message: payload.message, userId: payload.userId, roles: payload.roles, error: error.message, }); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to send notification' ); } } ``` --- ### 7. **RankController, RelationshipController, ReligionController** - No Error Handling **File & Location:** - [RankController.ts](src/controllers/RankController.ts) - [RelationshipController.ts](src/controllers/RelationshipController.ts) - [ReligionController.ts](src/controllers/ReligionController.ts) **Problem Type:** 2. Missing Error Handle **Root Cause:** All database operations in these controllers lack proper error handling. While they use HttpError for business logic validation, they don't handle database errors (connection issues, timeouts, etc.). **Affected Code Locations:** - All methods in all three controllers **Recommended Fix:** Add a generic error handling middleware or wrap each method with try-catch: ```typescript // Example for RankController @Post() async createRank( @Body() requestBody: CreateRank, @Request() request: RequestWithUser, ) { try { const checkName = await this.rankRepository.findOne({ where: { name: requestBody.name }, }); if (checkName) { throw new HttpError(HttpStatusCode.CONFLICT, "ชื่อนี้มีอยู่ในระบบแล้ว"); } const before = null; const rank = Object.assign(new Rank(), requestBody); rank.createdUserId = request.user.sub; rank.createdFullName = request.user.name; rank.lastUpdateUserId = request.user.sub; rank.lastUpdateFullName = request.user.name; rank.createdAt = new Date(); rank.lastUpdatedAt = new Date(); await this.rankRepository.save(rank, { data: request }); setLogDataDiff(request, { before, after: rank }); return new HttpSuccess(); } catch (error) { if (error instanceof HttpError) { throw error; } console.error('Error creating rank:', { name: requestBody.name, error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined, }); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to create rank' ); } } ``` --- ### 8. **ViewWorkFlowController** - Sequential Async Operations in Loop **File & Location:** [ViewWorkFlowController.ts](src/controllers/ViewWorkFlowController.ts) - Method: `getSystems()` **Problem Type:** 2. Missing Error Handle **Root Cause:** Uses a for loop to process items sequentially, which could be slow and doesn't handle errors for individual items. **Affected Code Locations:** - Line 41-49: Sequential for loop processing **Code Example:** ```typescript const sys: any = []; for (let index = 0; index < lists.length; index++) { const element = await lists[index]; if (sys.findIndex((x: any) => x.sysName === element.sysName) === -1) { sys.push({ sysName: element.sysName, name: element.name, }); } } ``` **Recommended Fix:** ```typescript @Get("lists") public async getSystems(@Request() req: RequestWithUser) { try { const lists = await this.metaWorkflowRepository .createQueryBuilder("metaWorkflow") .select(["metaWorkflow.name", "metaWorkflow.sysName"]) .getMany(); // Use Map for better performance and automatic deduplication const sysMap = new Map(); for (const element of lists) { if (!sysMap.has(element.sysName)) { sysMap.set(element.sysName, { sysName: element.sysName, name: element.name, }); } } const sys = Array.from(sysMap.values()); return new HttpSuccess(sys); } catch (error: any) { console.error('Error getting workflow systems:', { error: error.message, stack: error.stack, }); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to retrieve workflow systems' ); } } ``` --- ## Summary Statistics **Total Critical Issues Found:** 8 **Breakdown by Type:** - **Unhandled Exception (forEach/for await with async):** 5 instances - **Missing Error Handling (DB operations):** 10 instances - **Transaction Issues:** 1 instance - **External API Error Handling:** 2 instances - **Generic Error Handling:** 3 instances **Controllers with Issues:** 1. UserController - 5 critical issues 2. WorkflowController - 2 critical issues 3. ScriptProfileOrgController - 2 critical issues 4. SubDistrictController - 1 issue 5. SocketController - 1 issue 6. RankController - 1 issue 7. RelationshipController - 1 issue 8. ReligionController - 1 issue 9. ViewWorkFlowController - 1 issue 10. ReportController - Not fully analyzed (file too large) **Risk Level: HIGH** --- ## Priority Recommendations ### Immediate Actions Required: 1. **Fix UserController methods** - Add comprehensive error handling to all `for await` loops and Keycloak operations 2. **Add transactions to WorkflowController** - Ensure data consistency during workflow creation 3. **Improve external API error handling** - Add proper logging and retry logic for external service calls 4. **Add global error handling middleware** - Catch unhandled errors at the application level 5. **Implement circuit breakers** - For external dependencies (Keycloak, leave service) ### Graceful Recovery Strategies: 1. **Implement request-level error boundaries** - Catch errors at the controller level 2. **Add operation timeouts** - Prevent indefinite hangs on external API calls 3. **Implement retry logic with exponential backoff** - For transient failures 4. **Add health checks** - Monitor Keycloak and database connectivity 5. **Use connection pooling** with proper error handling ### Long-term Improvements: 1. **Implement a centralized error handling middleware** 2. **Add structured logging** (e.g., Winston, Pino) 3. **Implement request tracing** for debugging distributed issues 4. **Add metrics/monitoring** for error rates and external API failures 5. **Implement graceful shutdown** procedures for batch operations --- ## Testing Recommendations 1. **Test Keycloak failure scenarios** - Simulate Keycloak unavailability during user operations 2. **Test with large datasets** - Ensure for await operations don't cause memory issues 3. **Test transaction rollback** - Verify data consistency on errors 4. **Test concurrent requests** - Ensure race conditions don't cause crashes 5. **Test external API failures** - Simulate leave service and notification failures 6. **Test database connection failures** - Ensure proper handling of connection issues