46 KiB
Batch 14 Controllers Analysis (Controllers 131-140)
Controllers in this batch:
- RankController
- RelationshipController
- ReligionController
- ReportController (partial - file too large, analyzed first 100 lines)
- ScriptProfileOrgController
- SocketController
- SubDistrictController
- UserController
- ViewWorkFlowController
- WorkflowController
Critical Issues Found
1. UserController - Multiple Unhandled forEach Async Operations
File & Location: 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 awaitloop increateUserImport()- No error handling for individual user creation failures - Line 1133-1148:
for awaitloop inchangeUserPasswordAll()- Errors are silently ignored but not properly handled - Line 1169-1227:
for awaitloop inaddroleStaffToUser()- Keycloak operations without error handling - Line 1249-1307:
for awaitloop inaddroleStaffToUserEmp()- Keycloak operations without error handling - Line 1066-1118:
Promise.all()increateUserImportEmp()- Limited error handling
Code Examples:
// 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
}
// 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:
// 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 - 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:
// 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);
}
// 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);
}
});
// 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:
@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 - 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:
// 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,
});
// 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:
// 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 - 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:
// 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:
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 - 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:
// 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:
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 - 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:
@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:
@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:
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:
// 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 - 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:
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:
@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<string, { sysName: string; name: string }>();
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:
- UserController - 5 critical issues
- WorkflowController - 2 critical issues
- ScriptProfileOrgController - 2 critical issues
- SubDistrictController - 1 issue
- SocketController - 1 issue
- RankController - 1 issue
- RelationshipController - 1 issue
- ReligionController - 1 issue
- ViewWorkFlowController - 1 issue
- ReportController - Not fully analyzed (file too large)
Risk Level: HIGH
Priority Recommendations
Immediate Actions Required:
- Fix UserController methods - Add comprehensive error handling to all
for awaitloops and Keycloak operations - Add transactions to WorkflowController - Ensure data consistency during workflow creation
- Improve external API error handling - Add proper logging and retry logic for external service calls
- Add global error handling middleware - Catch unhandled errors at the application level
- Implement circuit breakers - For external dependencies (Keycloak, leave service)
Graceful Recovery Strategies:
- Implement request-level error boundaries - Catch errors at the controller level
- Add operation timeouts - Prevent indefinite hangs on external API calls
- Implement retry logic with exponential backoff - For transient failures
- Add health checks - Monitor Keycloak and 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 distributed issues
- Add metrics/monitoring for error rates and external API failures
- Implement graceful shutdown procedures for batch operations
Testing Recommendations
- Test Keycloak failure scenarios - Simulate Keycloak unavailability during user operations
- Test with large datasets - Ensure for await 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 external API failures - Simulate leave service and notification failures
- Test database connection failures - Ensure proper handling of connection issues