diff --git a/src/controllers/AuthRoleController.ts b/src/controllers/AuthRoleController.ts index 4159c5ec..13f5ce04 100644 --- a/src/controllers/AuthRoleController.ts +++ b/src/controllers/AuthRoleController.ts @@ -123,18 +123,25 @@ export class AuthRoleController extends Controller { // เช็คว่าถ้ามีค่า current_holderId ให้ลบ key สิทธิ์ใน redis if (posMaster.current_holderId) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); - redisClient.del("role_" + posMaster.current_holderId, (err: Error, response: Response) => { - if (err) throw err; - }); + redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { + if (err) console.error("Redis delete role error:", err); + }); - redisClient.del("menu_" + posMaster.current_holderId, (err: Error, response: Response) => { - if (err) throw err; - }); + redisClient.del("menu_" + posMaster.current_holderId, (err: Error) => { + if (err) console.error("Redis delete menu error:", err); + }); + } finally { + if (redisClient) { + redisClient.quit(); + } + } } return new HttpSuccess(); @@ -260,20 +267,45 @@ export class AuthRoleController extends Controller { return newAttr; }); const before = structuredClone(record); - await Promise.all([ - this.authRoleRepo.save(record, { data: req }), - setLogDataDiff(req, { before, after: record }), - ...newAttrs.map((attr) => this.authRoleAttrRepo.save(attr)), - ]); - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); + const queryRunner = AppDataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - await redisClient.flushdb(function (err: any, succeeded: any) { - console.log(succeeded); // will be true if successfull - }); + try { + await queryRunner.manager.save(AuthRole, record); + await Promise.all( + newAttrs.map((attr) => queryRunner.manager.save(AuthRoleAttr, attr)) + ); + await queryRunner.commitTransaction(); + + setLogDataDiff(req, { before, after: record }); + } catch (error) { + await queryRunner.rollbackTransaction(); + console.error("Error saving auth role:", error); + throw new HttpError( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาดในการบันทึกข้อมูลบทบาท กรุณาลองใหม่ในภายหลัง" + ); + } finally { + await queryRunner.release(); + } + + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + + await redisClient.flushdb(function (err: any, succeeded: any) { + console.log(succeeded); // will be true if successfull + }); + } finally { + if (redisClient) { + redisClient.quit(); + } + } return new HttpSuccess(); } diff --git a/src/controllers/CommandOperatorController.ts b/src/controllers/CommandOperatorController.ts index 1a461ab3..18393f53 100644 --- a/src/controllers/CommandOperatorController.ts +++ b/src/controllers/CommandOperatorController.ts @@ -9,7 +9,7 @@ import { Path, Request, Response, - Get + Get, } from "tsoa"; import { LessThan, MoreThan } from "typeorm"; import { AppDataSource } from "../database/data-source"; @@ -37,9 +37,7 @@ export class CommandOperatorController extends Controller { * @param commandId คีย์คำสั่ง */ @Get("{commandId}") - async getCommandOperatorByCommandId( - @Path() commandId: string - ) { + async getCommandOperatorByCommandId(@Path() commandId: string) { const command = await this.commandRepo.findOne({ where: { id: commandId }, select: { id: true }, @@ -61,10 +59,7 @@ export class CommandOperatorController extends Controller { * @param operatorId คีย์เจ้าหน้าที่ดำเนินการ */ @Get("swap/{direction}/{operatorId}") - async swapCommandOperator( - @Path() direction: string, - @Path() operatorId: string, - ) { + async swapCommandOperator(@Path() direction: string, @Path() operatorId: string) { const source = await this.commandOperatorRepo.findOne({ where: { id: operatorId }, }); @@ -106,10 +101,7 @@ export class CommandOperatorController extends Controller { source.orderNo = dest.orderNo; dest.orderNo = temp; - await Promise.all([ - this.commandOperatorRepo.save(source), - this.commandOperatorRepo.save(dest), - ]); + await Promise.all([this.commandOperatorRepo.save(source), this.commandOperatorRepo.save(dest)]); return new HttpSuccess(); } @@ -141,35 +133,29 @@ export class CommandOperatorController extends Controller { const nextOrderNo = (lastOrderNo?.orderNo ?? 1) + 1; const now = new Date(); - const operator = Object.assign( - new CommandOperator(), - { - ...body, - commandId: command.id, - orderNo: nextOrderNo, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - } - ); + const operator = Object.assign(new CommandOperator(), { + ...body, + commandId: command.id, + orderNo: nextOrderNo, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + }); await this.commandOperatorRepo.save(operator); return new HttpSuccess(); } - /** + /** * API ลบเจ้าหน้าที่ดำเนินการที่คำสั่ง * @summary API ลบเจ้าหน้าที่ดำเนินการที่คำสั่ง * @param commandId คีย์คำสั่ง * @param operatorId คีย์เจ้าหน้าที่ดำเนินการ */ @Delete("{commandId}/{operatorId}") - public async deleteCommandOperator( - @Path() commandId: string, - @Path() operatorId: string, - ) { + public async deleteCommandOperator(@Path() commandId: string, @Path() operatorId: string) { const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -215,10 +201,9 @@ export class CommandOperatorController extends Controller { return new HttpSuccess(true); } catch (error) { await queryRunner.rollbackTransaction(); - throw error; + console.error("Delete command operator error:", error); } finally { await queryRunner.release(); } } - } diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 960b4815..e107df5c 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -214,6 +214,7 @@ export class OrganizationController extends Controller { await sendToQueueOrgDraft(msg); return new HttpSuccess("Draft is being created... Processing in the background."); } catch (error: any) { + console.error("Error creating draft organization:", error); throw error; } } @@ -2529,6 +2530,7 @@ export class OrganizationController extends Controller { await sendToQueueOrg(msg); return new HttpSuccess(); } catch (error: any) { + console.error("Error publishing draft organization:", error); throw error; } } diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 8c713947..44747b09 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -37,11 +37,13 @@ export class PermissionController extends Controller { @Get("") public async getPermission(@Request() request: RequestWithUser) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ select: ["id"], @@ -270,6 +272,11 @@ export class PermissionController extends Controller { redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); } return new HttpSuccess(reply); + } finally { + if (redisClient) { + redisClient.quit(); + } + } } @Get("menu") @@ -281,11 +288,13 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -438,6 +447,11 @@ export class PermissionController extends Controller { } return new HttpSuccess(reply); + } finally { + if (redisClient) { + redisClient.quit(); + } + } } /** @@ -672,11 +686,13 @@ export class PermissionController extends Controller { @Path() system: string, @Path() action: string, ) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -765,6 +781,11 @@ export class PermissionController extends Controller { } return new HttpSuccess(reply); + } finally { + if (redisClient) { + redisClient.quit(); + } + } } @Get("user/{system}/{action}/{id}") @@ -781,11 +802,13 @@ export class PermissionController extends Controller { orgRevisionIsCurrent: true, }, }); - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let org = this.PermissionOrg(request, system, action); let reply = await getAsync("user_" + id); @@ -866,14 +889,21 @@ export class PermissionController extends Controller { } return new HttpSuccess(reply); + } finally { + if (redisClient) { + redisClient.quit(); + } + } } public async getPermissionFunc(@Request() request: RequestWithUser) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profile: any = await this.profileRepo.findOne({ select: ["id"], @@ -1090,6 +1120,11 @@ export class PermissionController extends Controller { redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply)); } return reply; + } finally { + if (redisClient) { + redisClient.quit(); + } + } } public async Permission(req: RequestWithUser, system: string, action: string) { @@ -1115,11 +1150,13 @@ export class PermissionController extends Controller { } public async listAuthSysOrgFunc(request: RequestWithUser, system: string, action: string) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + const getAsync = promisify(redisClient.get).bind(redisClient); let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -1187,6 +1224,11 @@ export class PermissionController extends Controller { redisClient.setex("posMaster_" + profile.id, 86400, JSON.stringify(reply)); } return reply; + } finally { + if (redisClient) { + redisClient.quit(); + } + } } // Helper method: ดึง org scope จากตำแหน่งปกติ @@ -1366,11 +1408,13 @@ export class PermissionController extends Controller { @Get("checkOrg/{keycloakId}") public async checkOrg(@Path() keycloakId: string) { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - // const getAsync = promisify(redisClient.get).bind(redisClient); + let redisClient; + try { + redisClient = await this.redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + // const getAsync = promisify(redisClient.get).bind(redisClient); // let profileType = "OFFICER"; let profile: any = await this.profileRepo.findOne({ @@ -1448,5 +1492,10 @@ export class PermissionController extends Controller { // } return new HttpSuccess(reply); + } finally { + if (redisClient) { + redisClient.quit(); + } + } } } diff --git a/src/scripts/ClearOldOrgRevision.ts b/src/scripts/ClearOldOrgRevision.ts new file mode 100644 index 00000000..4d2be7a7 --- /dev/null +++ b/src/scripts/ClearOldOrgRevision.ts @@ -0,0 +1,27 @@ +import "dotenv/config"; +import "reflect-metadata"; +import { AppDataSource } from "../database/data-source"; +import { clearOldOrgRevisionData } from "../services/ClearOldOrgRevisionService"; + +// "clear:old-org-revision": "ts-node src/scripts/ClearOldOrgRevision.ts", + +const defaultOrgRevisionId = "24dacf63-d289-496c-8102-8b25079dbaf2"; + +async function main(): Promise { + const orgRevisionId = process.argv[2] || defaultOrgRevisionId; + + try { + await AppDataSource.initialize(); + const result = await clearOldOrgRevisionData(orgRevisionId); + console.info(JSON.stringify(result, null, 2)); + } catch (error) { + console.error("[ClearOldOrgRevision] Failed:", error); + process.exitCode = 1; + } finally { + if (AppDataSource.isInitialized) { + await AppDataSource.destroy(); + } + } +} + +void main(); diff --git a/src/services/ClearOldOrgRevisionService.ts b/src/services/ClearOldOrgRevisionService.ts new file mode 100644 index 00000000..056206f3 --- /dev/null +++ b/src/services/ClearOldOrgRevisionService.ts @@ -0,0 +1,232 @@ +import { EntityManager, EntityTarget, In } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import { OrgRevision } from "../entities/OrgRevision"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgChild1 } from "../entities/OrgChild1"; +import { OrgChild2 } from "../entities/OrgChild2"; +import { OrgChild3 } from "../entities/OrgChild3"; +import { OrgChild4 } from "../entities/OrgChild4"; +import { PosMasterAct } from "../entities/PosMasterAct"; +import { PosMasterAssign } from "../entities/PosMasterAssign"; +import { PermissionOrg } from "../entities/PermissionOrg"; +import { PermissionProfile } from "../entities/PermissionProfile"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster"; +import { EmployeePosition } from "../entities/EmployeePosition"; +import { orgStructureCache } from "../utils/OrgStructureCache"; + +export interface ClearOldOrgRevisionSummary { + orgRevisionId: string; + orgRevisionName: string; + deleted: { + positions: number; + employeePositionsByPosMaster: number; + employeePositionsByTempPosMaster: number; + posMasterActsByParent: number; + posMasterActsByChild: number; + posMasterAssigns: number; + posMasters: number; + employeePosMasters: number; + employeeTempPosMasters: number; + permissionOrgs: number; + permissionProfiles: number; + orgChild4s: number; + orgChild3s: number; + orgChild2s: number; + orgChild1s: number; + orgRoots: number; + orgRevisions: number; + }; +} + +interface OrgRevisionSnapshot { + id: string; + orgRevisionName: string; + orgRevisionIsCurrent: boolean; + orgRevisionIsDraft: boolean; +} + +export async function clearOldOrgRevisionData( + orgRevisionId: string, +): Promise { + const result = await AppDataSource.transaction(async (manager) => { + const orgRevision = await manager.findOne(OrgRevision, { + where: { id: orgRevisionId }, + select: ["id", "orgRevisionName", "orgRevisionIsCurrent", "orgRevisionIsDraft"], + }); + + if (!orgRevision) { + throw new Error(`ไม่พบ orgRevision ที่ต้องการล้างข้อมูล: ${orgRevisionId}`); + } + + validateOrgRevisionForDeletion(orgRevision); + + const [posMasters, orgRoots, employeePosMasters, employeeTempPosMasters] = await Promise.all([ + manager.find(PosMaster, { + where: { orgRevisionId }, + select: ["id"], + }), + manager.find(OrgRoot, { + where: { orgRevisionId }, + select: ["id"], + }), + manager.find(EmployeePosMaster, { + where: { orgRevisionId }, + select: ["id"], + }), + manager.find(EmployeeTempPosMaster, { + where: { orgRevisionId }, + select: ["id"], + }), + ]); + + const posMasterIds = posMasters.map((item) => item.id); + const orgRootIds = orgRoots.map((item) => item.id); + const employeePosMasterIds = employeePosMasters.map((item) => item.id); + const employeeTempPosMasterIds = employeeTempPosMasters.map((item) => item.id); + + const [ + positionsCount, + employeePositionsByPosMasterCount, + employeePositionsByTempPosMasterCount, + posMasterActsByParentCount, + posMasterActsByChildCount, + posMasterAssignsCount, + permissionOrgsCount, + permissionProfilesCount, + orgChild4sCount, + orgChild3sCount, + orgChild2sCount, + orgChild1sCount, + ] = await Promise.all([ + countByIds(manager, Position, "posMasterId", posMasterIds), + countByIds(manager, EmployeePosition, "posMasterId", employeePosMasterIds), + countByIds(manager, EmployeePosition, "posMasterTempId", employeeTempPosMasterIds), + countByIds(manager, PosMasterAct, "posMasterId", posMasterIds), + countByIds(manager, PosMasterAct, "posMasterChildId", posMasterIds), + countByIds(manager, PosMasterAssign, "posMasterId", posMasterIds), + countByIds(manager, PermissionOrg, "orgRootId", orgRootIds), + countByIds(manager, PermissionProfile, "orgRootId", orgRootIds), + manager.count(OrgChild4, { where: { orgRevisionId } }), + manager.count(OrgChild3, { where: { orgRevisionId } }), + manager.count(OrgChild2, { where: { orgRevisionId } }), + manager.count(OrgChild1, { where: { orgRevisionId } }), + ]); + + if (positionsCount > 0) { + await manager.delete(Position, { posMasterId: In(posMasterIds) }); + } + if (employeePositionsByPosMasterCount > 0) { + await manager.delete(EmployeePosition, { posMasterId: In(employeePosMasterIds) }); + } + if (employeePositionsByTempPosMasterCount > 0) { + await manager.delete(EmployeePosition, { posMasterTempId: In(employeeTempPosMasterIds) }); + } + if (posMasterActsByParentCount > 0) { + await manager.delete(PosMasterAct, { posMasterId: In(posMasterIds) }); + } + if (posMasterActsByChildCount > 0) { + await manager.delete(PosMasterAct, { posMasterChildId: In(posMasterIds) }); + } + if (posMasterAssignsCount > 0) { + await manager.delete(PosMasterAssign, { posMasterId: In(posMasterIds) }); + } + + const posMastersCount = posMasterIds.length; + const employeePosMastersCount = employeePosMasterIds.length; + const employeeTempPosMastersCount = employeeTempPosMasterIds.length; + + if (posMastersCount > 0) { + await manager.delete(PosMaster, { orgRevisionId }); + } + if (employeePosMastersCount > 0) { + await manager.delete(EmployeePosMaster, { orgRevisionId }); + } + if (employeeTempPosMastersCount > 0) { + await manager.delete(EmployeeTempPosMaster, { orgRevisionId }); + } + + if (permissionOrgsCount > 0) { + await manager.delete(PermissionOrg, { orgRootId: In(orgRootIds) }); + } + if (permissionProfilesCount > 0) { + await manager.delete(PermissionProfile, { orgRootId: In(orgRootIds) }); + } + + if (orgChild4sCount > 0) { + await manager.delete(OrgChild4, { orgRevisionId }); + } + if (orgChild3sCount > 0) { + await manager.delete(OrgChild3, { orgRevisionId }); + } + if (orgChild2sCount > 0) { + await manager.delete(OrgChild2, { orgRevisionId }); + } + if (orgChild1sCount > 0) { + await manager.delete(OrgChild1, { orgRevisionId }); + } + + const orgRootsCount = orgRootIds.length; + if (orgRootsCount > 0) { + await manager.delete(OrgRoot, { orgRevisionId }); + } + + await manager.delete(OrgRevision, { id: orgRevisionId }); + + return { + orgRevisionId: orgRevision.id, + orgRevisionName: orgRevision.orgRevisionName, + deleted: { + positions: positionsCount, + employeePositionsByPosMaster: employeePositionsByPosMasterCount, + employeePositionsByTempPosMaster: employeePositionsByTempPosMasterCount, + posMasterActsByParent: posMasterActsByParentCount, + posMasterActsByChild: posMasterActsByChildCount, + posMasterAssigns: posMasterAssignsCount, + posMasters: posMastersCount, + employeePosMasters: employeePosMastersCount, + employeeTempPosMasters: employeeTempPosMastersCount, + permissionOrgs: permissionOrgsCount, + permissionProfiles: permissionProfilesCount, + orgChild4s: orgChild4sCount, + orgChild3s: orgChild3sCount, + orgChild2s: orgChild2sCount, + orgChild1s: orgChild1sCount, + orgRoots: orgRootsCount, + orgRevisions: 1, + }, + }; + }); + + orgStructureCache.invalidate(orgRevisionId); + return result; +} + +function validateOrgRevisionForDeletion(orgRevision: OrgRevisionSnapshot): void { + if (orgRevision.orgRevisionIsCurrent) { + throw new Error(`ไม่สามารถลบ orgRevision ปัจจุบันได้: ${orgRevision.id}`); + } + + if (orgRevision.orgRevisionIsDraft) { + throw new Error(`ไม่สามารถลบ orgRevision แบบร่างได้ด้วยสคริปต์นี้: ${orgRevision.id}`); + } +} + +async function countByIds( + manager: EntityManager, + entity: EntityTarget, + field: keyof Entity, + ids: string[], +): Promise { + if (ids.length === 0) { + return 0; + } + + const alias = "entity"; + return manager + .createQueryBuilder(entity, alias) + .where(`${alias}.${String(field)} IN (:...ids)`, { ids }) + .getCount(); +} diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 29bb168a..b6514eca 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -188,6 +188,7 @@ export async function CreatePosMasterHistoryOfficer( return true; } catch (err) { if (manager) { + console.error("CreatePosMasterHistoryOfficer error (external transaction):", err); throw err; } console.error("CreatePosMasterHistoryOfficer transaction error:", err);