25 KiB
Batch 13 Controllers Analysis (Controllers 121-130)
Controllers in this batch:
- ProfileOtherEmployeeController
- ProfileOtherEmployeeTempController
- ProfileSalaryController
- ProfileSalaryEmployeeController
- ProfileSalaryEmployeeTempController
- ProfileSalaryTempController
- ProfileTrainingController
- ProfileTrainingEmployeeController
- ProfileTrainingEmployeeTempController
- ProvinceController
Critical Issues Found
1. ProfileSalaryTempController - Multiple Unhandled forEach Async Operations
File & Location: 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) => { ... })indeleteSalary() - Line 1115-1118:
salaryList.forEach(async (p, i) => { ... })indeleteSalary() - Line 202-205:
salaryOld.forEach((item: any, i) => { ... })inlistSalary()(sync operations but no error handling) - Line 1729-1741:
for awaitloop with database operations without error handling inchangeSortEditGenAll() - Line 1763-1766:
salaryOld.forEach()inchangeSortEdit()
Code Examples:
// 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
});
// 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:
// 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 - 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) => { ... })indeleteSalary() - 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) => { ... })withPromise.all()but no error handling - Line 463-477: Similar pattern in
getSalaryPositionUser() - Line 497-512: Similar pattern in
getSalary()
Code Examples:
// 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
});
// 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:
// 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 - 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(?, ?)incronjobTenurePositionOfficer() - Line 126-129: Similar in
cronjobTenurePositionEmployee() - Line 176-179:
CALL GetProfileSalaryLevel(?, ?)incronjobTenureLevelOfficer() - Line 236-239: Similar in
cronjobTenureLevelEmployee() - Line 317-320:
CALL GetProfileSalaryExecutive(?, ?)incronjobTenureExecutivePositionOfficer() - Line 588-591, 622-625, 662-665: Multiple calls in
getPositionTenureUser() - Line 722-725, 760-763, 803-806: Multiple calls in
getPositionTenure()
Code Examples:
// Line 76-79 - No error handling for stored procedure call
const position = await AppDataSource.query("CALL GetProfileSalaryPosition(?, ?)", [
x.id,
_currentDate,
]);
Recommended Fix:
// 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 - 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:
// 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:
@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 - 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) => { ... })indeleteSalaryEmployee()
Code Example:
// Line 608-611 - DANGEROUS
salaryList.forEach(async (p, i) => {
p.order = i + 1;
await this.salaryRepo.save(p); // Unhandled rejection
});
Recommended Fix:
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 - 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) => { ... })indeleteSalaryEmployee()
Recommended Fix: Same as above - use Promise.all() with error handling.
7. ProfileSalaryTempController - confirmDoneSalary() Transaction Handling Issues
File & Location: ProfileSalaryTempController.ts - Method: confirmDoneSalary()
Problem Type: 2. Missing Error Handle
Root Cause: While this method uses transactions, there are several potential issues:
- Line 1686: Empty
catchblock that swallows all errors - Line 1493-1497: Error is re-thrown without proper logging or context
- Multiple complex operations within transaction that could fail
Affected Code Locations:
- Line 1493-1498:
catchblock re-throws error without logging - Line 1685: Empty
catchblock inreturnEdit()
Code Examples:
// Line 1493-1498 - Insufficient error handling
} catch (error) {
await queryRunner.rollbackTransaction();
throw error; // No logging, no context
} finally {
await queryRunner.release();
}
Recommended Fix:
} 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 - 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:
// Line 1425-1431 - Bulk insert without error handling
if (salaryRows.length) {
await queryRunner.manager.insert(
ProfileSalary,
salaryRows.map(({ id, ...data }) => ({
...data,
...metaCreated,
})),
);
}
Recommended Fix:
// 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 - 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:
// 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:
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:
- ProfileSalaryTempController - 4 critical issues
- ProfileSalaryController - 3 critical issues
- ProfileSalaryEmployeeController - 1 critical issue
- ProfileSalaryEmployeeTempController - 1 critical issue
- ProfileTrainingController - 2 critical issues
- ProvinceController - 1 minor issue
Risk Level: HIGH
Priority Recommendations
Immediate Actions Required:
- Replace all
forEach()with async operations - UsePromise.all()orfor...ofloops with proper error handling - Add error boundaries around all database operations
- Implement proper logging for all errors
- Use transactions for multi-step database operations
- Add circuit breakers for external dependencies (database, stored procedures)
Graceful Recovery Strategies:
- Implement request-level error boundaries - Catch errors at the controller level and return appropriate HTTP responses
- Add database operation timeouts - Prevent indefinite hangs
- Implement retry logic for transient database errors
- Add health checks - Monitor database connectivity
- Use connection pooling with proper error handling
Long-term Improvements:
- Implement a centralized error handling middleware
- Add structured logging (e.g., Winston, Pino)
- Implement request tracing for debugging
- Add metrics/monitoring for error rates
- Implement graceful shutdown procedures
Testing Recommendations
- Test database failure scenarios - Disconnect database during operations
- Test with large datasets - Ensure forEach operations don't cause memory issues
- Test transaction rollback - Verify data consistency on errors
- Test concurrent requests - Ensure race conditions don't cause crashes
- Test stored procedure failures - Simulate SP errors