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

1422 lines
46 KiB
Markdown

# 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<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:**
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