1423 lines
46 KiB
Markdown
1423 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
|