# Batch 13 Controllers Analysis (Controllers 121-130) ## Controllers in this batch: 1. ProfileOtherEmployeeController 2. ProfileOtherEmployeeTempController 3. ProfileSalaryController 4. ProfileSalaryEmployeeController 5. ProfileSalaryEmployeeTempController 6. ProfileSalaryTempController 7. ProfileTrainingController 8. ProfileTrainingEmployeeController 9. ProfileTrainingEmployeeTempController 10. ProvinceController --- ## Critical Issues Found ### 1. **ProfileSalaryTempController** - Multiple Unhandled forEach Async Operations **File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) - Methods: `listSalary()`, `confirmDoneSalary()`, `changeSortEditGenAll()`, `changeSortEdit()` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** Multiple methods use `forEach()` with async operations without proper error handling or awaiting. When errors occur in these async callbacks, they become unhandled rejections that can crash the Node.js process. **Affected Code Locations:** - Line 1058-1061: `salaryOld.forEach(async (p, i) => { ... })` in `deleteSalary()` - Line 1115-1118: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalary()` - Line 202-205: `salaryOld.forEach((item: any, i) => { ... })` in `listSalary()` (sync operations but no error handling) - Line 1729-1741: `for await` loop with database operations without error handling in `changeSortEditGenAll()` - Line 1763-1766: `salaryOld.forEach()` in `changeSortEdit()` **Code Examples:** ```typescript // Line 1115-1118 - DANGEROUS: async forEach without error handling salaryList.forEach(async (p, i) => { p.order = i + 1; await this.salaryRepo.save(p); // If this fails, error is unhandled }); ``` ```typescript // Line 1729-1741 - DANGEROUS: for await without try-catch for await (const item of profiles) { let salaryOld = await this.salaryOldRepo.find({ where: { profileId: item.id }, order: { commandDateAffect: "ASC", order: "ASC" }, }); salaryOld.forEach((item: any, i) => { item.order = i + 1; }); num = num + 1; console.log(num); await this.salaryOldRepo.save(salaryOld); // If this fails, entire operation crashes } ``` **Recommended Fix:** ```typescript // For deleteSalary() - Use Promise.all with error handling try { await Promise.all( salaryList.map(async (p, i) => { p.order = i + 1; await this.salaryRepo.save(p); }) ); } catch (error) { console.error('Error updating salary order:', error); // Optionally throw a more specific error or handle gracefully throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to update salary order'); } // For changeSortEditGenAll() - Add error handling per iteration try { const profiles = await this.profileRepo.find(); let num = 1; for await (const item of profiles) { try { let salaryOld = await this.salaryOldRepo.find({ where: { profileId: item.id }, order: { commandDateAffect: "ASC", order: "ASC" }, }); salaryOld.forEach((item: any, i) => { item.order = i + 1; }); await this.salaryOldRepo.save(salaryOld); num = num + 1; console.log(num); } catch (error) { console.error(`Error processing profile ${item.id}:`, error); // Continue with next profile instead of crashing continue; } } return new HttpSuccess(); } catch (error) { console.error('Error in changeSortEditGenAll:', error); throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to process profiles'); } ``` --- ### 2. **ProfileSalaryController** - Unhandled forEach Async Operations **File & Location:** [ProfileSalaryController.ts](src/controllers/ProfileSalaryController.ts) - Methods: `deleteSalary()`, `Registry()`, `RegistryEmployee()` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** Multiple critical methods use `forEach()` with async database operations. When database operations fail within these callbacks, the Promise rejection is unhandled and can crash the service. **Affected Code Locations:** - Line 1115-1118: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalary()` - Line 362-373: Complex async operations in `Registry()` without error handling - Line 383-395: Complex async operations in `RegistryEmployee()` without error handling - Line 412-427: `record.map(async (r) => { ... })` with `Promise.all()` but no error handling - Line 463-477: Similar pattern in `getSalaryPositionUser()` - Line 497-512: Similar pattern in `getSalary()` **Code Examples:** ```typescript // Line 1115-1118 - CRITICAL: async forEach without error handling salaryList.forEach(async (p, i) => { p.order = i + 1; await this.salaryRepo.save(p); // Unhandled rejection }); ``` ```typescript // Line 412-427 - Promise.all without error handling const result = await Promise.all( record.map(async (r) => { let _command = null; if (r.commandId) { _command = await this.commandRepository.findOne({ where: { id: r.commandId }, relations: ["commandType"], }); } return { ...r, commandType: _command && _command?.commandType ? _command?.commandType.code : null, }; }), ); ``` **Recommended Fix:** ```typescript // For deleteSalary() - Proper error handling try { await Promise.all( salaryList.map(async (p, i) => { p.order = i + 1; await this.salaryRepo.save(p); }) ); } catch (error) { console.error('Error updating salary order:', error); throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to update salary order'); } // For Promise.all operations - Add error boundary try { const result = await Promise.all( record.map(async (r) => { try { let _command = null; if (r.commandId) { _command = await this.commandRepository.findOne({ where: { id: r.commandId }, relations: ["commandType"], }); } return { ...r, commandType: _command && _command?.commandType ? _command?.commandType.code : null, }; } catch (error) { console.error(`Error loading command for salary ${r.id}:`, error); return { ...r, commandType: null, }; } }), ); return new HttpSuccess(result); } catch (error) { console.error('Error processing salary records:', error); throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to load salary data'); } // For Registry() - Add comprehensive error handling try { await this.registryRepo.clear(); const allRegis = await AppDataSource.getRepository(viewRegistryOfficer) .createQueryBuilder("registryOfficer") .getMany(); const profileIds = new Set((await this.profileRepo.find()).map((p) => p.id)); const mapData = allRegis .filter((x) => profileIds.has(x.profileId)) .map((x) => ({ ...x, isProbation: Boolean(x.isProbation), isLeave: Boolean(x.isLeave), isRetirement: Boolean(x.isRetirement), Educations: x.Educations ? JSON.stringify(x.Educations) : "", })); if (mapData.length > 0) { // Save in batches to avoid overwhelming the database const batchSize = 100; for (let i = 0; i < mapData.length; i += batchSize) { const batch = mapData.slice(i, i + batchSize); await this.registryRepo.save(batch); } } return new HttpSuccess(); } catch (error) { console.error('Error in Registry cronjob:', error); throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to sync registry data'); } ``` --- ### 3. **ProfileSalaryController** - Raw SQL Queries Without Error Handling **File & Location:** [ProfileSalaryController.ts](src/controllers/ProfileSalaryController.ts) - Multiple methods using `AppDataSource.query()` **Problem Type:** 2. Missing Error Handle **Root Cause:** Multiple stored procedure calls (`CALL GetProfile...()`) are executed without try-catch blocks. If these stored procedures fail or the database is unavailable, the errors will propagate unhandled. **Affected Code Locations:** - Line 76-79: `CALL GetProfileSalaryPosition(?, ?)` in `cronjobTenurePositionOfficer()` - Line 126-129: Similar in `cronjobTenurePositionEmployee()` - Line 176-179: `CALL GetProfileSalaryLevel(?, ?)` in `cronjobTenureLevelOfficer()` - Line 236-239: Similar in `cronjobTenureLevelEmployee()` - Line 317-320: `CALL GetProfileSalaryExecutive(?, ?)` in `cronjobTenureExecutivePositionOfficer()` - Line 588-591, 622-625, 662-665: Multiple calls in `getPositionTenureUser()` - Line 722-725, 760-763, 803-806: Multiple calls in `getPositionTenure()` **Code Examples:** ```typescript // Line 76-79 - No error handling for stored procedure call const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ x.id, _currentDate, ]); ``` **Recommended Fix:** ```typescript // Wrap all database query calls in try-catch try { const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [ x.id, _currentDate, ]); const _position = position.length > 0 ? position[0] : []; const mapPosition = _position.length > 1 ? _position.slice(1).map((curr: any, index: number) => ({ days_diff: curr.days_diff, positionName: _position[index]?.positionName, })) : []; const calDayDiff = mapPosition .filter((curr: any) => curr.positionName == x.position) .reduce( (acc: any, curr: any) => { acc.days_diff += Number(curr.days_diff) || 0; acc.positionName = curr.positionName; return acc; }, { days_diff: 0, positionName: null }, ); const { year, month, day } = calculateTenure(calDayDiff.days_diff); const mapData: any = { profileId: x.id, positionName: calDayDiff.positionName, days_diff: calDayDiff.days_diff, Years: year, Months: month, Days: day, }; data.push(mapData); } catch (error) { console.error(`Error processing position tenure for profile ${x.id}:`, error); // Add default/error entry or skip this profile const mapData: any = { profileId: x.id, positionName: null, days_diff: 0, Years: 0, Months: 0, Days: 0, error: true, }; data.push(mapData); } ``` --- ### 4. **ProfileTrainingController** - Multiple Database Operations Without Error Handling **File & Location:** [ProfileTrainingController.ts](src/controllers/ProfileTrainingController.ts) - Methods: `deleteAllTraining()`, `deleteById()` **Problem Type:** 2. Missing Error Handle **Root Cause:** Multiple sequential delete operations without transaction or error handling. If intermediate operations fail, the database can be left in an inconsistent state. **Affected Code Locations:** - Line 238-259: `deleteAllTraining()` - Multiple delete operations without transaction - Line 274-339: `deleteById()` - Multiple delete operations without transaction **Code Examples:** ```typescript // Line 238-259 - No error handling or transaction const trainings = await this.trainingRepo.find({ select: { id: true }, where: { developmentId: reqBody.developmentId }, }); if (trainings.length > 0) { const trainingIds = trainings.map((x) => x.id); await this.trainingHistoryRepo.delete({ profileTrainingId: In(trainingIds), }); await this.trainingRepo.delete({ developmentId: reqBody.developmentId, }); } await this.developmentHistoryRepo.delete({ kpiDevelopmentId: reqBody.developmentId, }); await this.developmentRepo.delete({ kpiDevelopmentId: reqBody.developmentId }); ``` **Recommended Fix:** ```typescript @Post("delete-all") public async deleteAllTraining( @Body() reqBody: { developmentId: string }, @Request() req: RequestWithUser ) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const trainings = await queryRunner.manager.find(ProfileTraining, { select: { id: true }, where: { developmentId: reqBody.developmentId }, }); if (trainings.length > 0) { const trainingIds = trainings.map((x) => x.id); await queryRunner.manager.delete(ProfileTrainingHistory, { profileTrainingId: In(trainingIds), }); await queryRunner.manager.delete(ProfileTraining, { developmentId: reqBody.developmentId, }); } await queryRunner.manager.delete(ProfileDevelopmentHistory, { kpiDevelopmentId: reqBody.developmentId, }); await queryRunner.manager.delete(ProfileDevelopment, { kpiDevelopmentId: reqBody.developmentId }); await queryRunner.commitTransaction(); return new HttpSuccess(); } catch (error) { await queryRunner.rollbackTransaction(); console.error('Error deleting training data:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to delete training data' ); } finally { await queryRunner.release(); } } // Similar fix for deleteById() @Post("delete-byId") public async deleteById( @Body() reqBody: { type: string; profileId: string; developmentId: string; }, @Request() req: RequestWithUser ) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const type = reqBody.type?.trim().toUpperCase(); // 1. validate profile if (type === "OFFICER") { const profile = await queryRunner.manager.findOne(Profile, { where: { id: reqBody.profileId } }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } } else { const profile = await queryRunner.manager.findOne(ProfileEmployee, { where: { id: reqBody.profileId } }); if (!profile) { throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); } } const profileField = type === "OFFICER" ? "profileId" : "profileEmployeeId"; // 2. Find and delete ProfileTraining const trainings = await queryRunner.manager.find(ProfileTraining, { select: { id: true }, where: { developmentId: reqBody.developmentId, [profileField]: reqBody.profileId, }, }); if (trainings.length > 0) { const trainingIds = trainings.map(x => x.id); await queryRunner.manager.delete(ProfileTrainingHistory, { profileTrainingId: In(trainingIds), }); await queryRunner.manager.delete(ProfileTraining, { id: In(trainingIds), }); } // 3. Find and delete ProfileDevelopment const developments = await queryRunner.manager.find(ProfileDevelopment, { select: { id: true }, where: { kpiDevelopmentId: reqBody.developmentId, [profileField]: reqBody.profileId, }, }); if (developments.length > 0) { const devIds = developments.map(x => x.id); await queryRunner.manager.delete(ProfileDevelopmentHistory, { profileDevelopmentId: In(devIds), }); await queryRunner.manager.delete(ProfileDevelopment, { id: In(devIds), }); } await queryRunner.commitTransaction(); return new HttpSuccess(); } catch (error) { await queryRunner.rollbackTransaction(); console.error('Error deleting by ID:', error); if (error instanceof HttpError) { throw error; } throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to delete data' ); } finally { await queryRunner.release(); } } ``` --- ### 5. **ProfileSalaryEmployeeController** - forEach Async Operations Without Error Handling **File & Location:** [ProfileSalaryEmployeeController.ts](src/controllers/ProfileSalaryEmployeeController.ts) - Method: `deleteSalaryEmployee()` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** Similar to ProfileSalaryController, uses `forEach()` with async operations without proper error handling. **Affected Code Locations:** - Line 608-611: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalaryEmployee()` **Code Example:** ```typescript // Line 608-611 - DANGEROUS salaryList.forEach(async (p, i) => { p.order = i + 1; await this.salaryRepo.save(p); // Unhandled rejection }); ``` **Recommended Fix:** ```typescript try { await Promise.all( salaryList.map(async (p, i) => { p.order = i + 1; await this.salaryRepo.save(p); }) ); } catch (error) { console.error('Error updating salary order:', error); throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to update salary order'); } ``` --- ### 6. **ProfileSalaryEmployeeTempController** - forEach Async Operations Without Error Handling **File & Location:** [ProfileSalaryEmployeeTempController.ts](src/controllers/ProfileSalaryEmployeeTempController.ts) - Method: `deleteSalaryEmployee()` **Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle **Root Cause:** Same pattern as above - `forEach()` with async operations. **Affected Code Locations:** - Line 202-205: `salaryList.forEach(async (p, i) => { ... })` in `deleteSalaryEmployee()` **Recommended Fix:** Same as above - use `Promise.all()` with error handling. --- ### 7. **ProfileSalaryTempController** - confirmDoneSalary() Transaction Handling Issues **File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) - Method: `confirmDoneSalary()` **Problem Type:** 2. Missing Error Handle **Root Cause:** While this method uses transactions, there are several potential issues: 1. Line 1686: Empty `catch` block that swallows all errors 2. Line 1493-1497: Error is re-thrown without proper logging or context 3. Multiple complex operations within transaction that could fail **Affected Code Locations:** - Line 1493-1498: `catch` block re-throws error without logging - Line 1685: Empty `catch` block in `returnEdit()` **Code Examples:** ```typescript // Line 1493-1498 - Insufficient error handling } catch (error) { await queryRunner.rollbackTransaction(); throw error; // No logging, no context } finally { await queryRunner.release(); } ``` **Recommended Fix:** ```typescript } catch (error) { await queryRunner.rollbackTransaction(); console.error('Error in confirmDoneSalary:', { profileId: body.profileId, type: body.type, error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined, }); // Provide more specific error message if (error instanceof HttpError) { throw error; } throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to confirm salary data. Please try again.' ); } finally { await queryRunner.release(); } // For returnEdit() - Proper error handling try { if (profile) { profile.statusCheckEdit = "PENDING"; await this.profileRepo.save(profile); } else if (profileEmployee) { profileEmployee.statusCheckEdit = "PENDING"; await this.profileEmployeeRepo.save(profileEmployee); } const history: PositionSalaryEditHistory = Object.assign( new PositionSalaryEditHistory(), body, ); if (profile) { history.profileId = profileId; } else if (profileEmployee) { history.profileEmployeeId = profileId; } history.returnedDate = new Date(); history.examinerName = req.user.name; history.createdFullName = req.user.name; history.lastUpdateFullName = req.user.name; await this.positionSalaryEditHistoryRepo.save(history); return new HttpSuccess(); } catch (error) { console.error('Error in returnEdit:', error); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, 'Failed to process return edit request' ); } ``` --- ### 8. **ProfileSalaryTempController** - Bulk Operations Without Error Handling **File & Location:** [ProfileSalaryTempController.ts](src/controllers/ProfileSalaryTempController.ts) - Methods: `listSalary()`, `confirmDoneSalary()` **Problem Type:** 2. Missing Error Handle **Root Cause:** Bulk insert operations without error handling for individual records. If one record fails, the entire operation may fail or data may be partially inserted. **Affected Code Locations:** - Line 1058-1061: `salaryOld.forEach()` without error handling - Line 1098-1101: Similar pattern - Line 1425-1431: Bulk insert without error handling **Code Example:** ```typescript // Line 1425-1431 - Bulk insert without error handling if (salaryRows.length) { await queryRunner.manager.insert( ProfileSalary, salaryRows.map(({ id, ...data }) => ({ ...data, ...metaCreated, })), ); } ``` **Recommended Fix:** ```typescript // Implement batch processing with error handling if (salaryRows.length) { const batchSize = 100; // Process in batches for (let i = 0; i < salaryRows.length; i += batchSize) { const batch = salaryRows.slice(i, i + batchSize); try { await queryRunner.manager.insert( ProfileSalary, batch.map(({ id, ...data }) => ({ ...data, ...metaCreated, })) ); } catch (error) { console.error(`Error inserting salary batch ${i / batchSize + 1}:`, error); // Log which records failed const failedIds = batch.map(b => b.id); console.error('Failed record IDs:', failedIds); throw new HttpError( HttpStatus.INTERNAL_SERVER_ERROR, `Failed to insert salary records (batch ${i / batchSize + 1})` ); } } } ``` --- ### 9. **ProvinceController** - Try-Catch With Generic Error Handling **File & Location:** [ProvinceController.ts](src/controllers/ProvinceController.ts) - Method: `Delete()` **Problem Type:** 2. Missing Error Handle **Root Cause:** While there is a try-catch block, it catches all errors without logging or differentiation. This makes debugging difficult and may mask underlying issues. **Affected Code Locations:** - Line 168-175: Generic catch block **Code Example:** ```typescript // Line 168-175 - Generic error handling let result: any; try { result = await this.provinceRepository.delete({ id: id }); } catch { throw new HttpError( HttpStatusCode.NOT_FOUND, "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลจังหวัดนี้อยู่", ); } ``` **Recommended Fix:** ```typescript let result: any; try { result = await this.provinceRepository.delete({ id: id }); } catch (error) { console.error('Error deleting province:', { id, error: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : undefined, }); // Check for foreign key constraint error if (error instanceof Error && error.message.includes('foreign key constraint')) { throw new HttpError( HttpStatusCode.CONFLICT, // Use 409 instead of 404 "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลจังหวัดนี้อยู่", ); } throw new HttpError( HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการลบข้อมูลจังหวัด", ); } ``` --- ## Summary Statistics **Total Critical Issues Found:** 9 **Breakdown by Type:** - **Unhandled Exception (forEach with async):** 6 instances - **Missing Error Handling (DB operations):** 8 instances - **Transaction Issues:** 2 instances - **Generic Error Handling:** 1 instance **Controllers with Issues:** 1. ProfileSalaryTempController - 4 critical issues 2. ProfileSalaryController - 3 critical issues 3. ProfileSalaryEmployeeController - 1 critical issue 4. ProfileSalaryEmployeeTempController - 1 critical issue 5. ProfileTrainingController - 2 critical issues 6. ProvinceController - 1 minor issue **Risk Level: HIGH** --- ## Priority Recommendations ### Immediate Actions Required: 1. **Replace all `forEach()` with async operations** - Use `Promise.all()` or `for...of` loops with proper error handling 2. **Add error boundaries** around all database operations 3. **Implement proper logging** for all errors 4. **Use transactions** for multi-step database operations 5. **Add circuit breakers** for external dependencies (database, stored procedures) ### Graceful Recovery Strategies: 1. **Implement request-level error boundaries** - Catch errors at the controller level and return appropriate HTTP responses 2. **Add database operation timeouts** - Prevent indefinite hangs 3. **Implement retry logic** for transient database errors 4. **Add health checks** - Monitor 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 4. **Add metrics/monitoring** for error rates 5. **Implement graceful shutdown** procedures --- ## Testing Recommendations 1. **Test database failure scenarios** - Disconnect database during operations 2. **Test with large datasets** - Ensure forEach 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 stored procedure failures** - Simulate SP errors