Merge branch 'fix/optimization-detailSuperAdmin' into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s

* fix/optimization-detailSuperAdmin:
  fix: api /super-admin/{id} memory cache
  fix: query use Promise all
  fix: Api GET /super-admin/{id}
  fix: เพิ่ม Graceful Shutdown - ป้องกัน connection in app file, Log Mnddleware + Memory Store
  fix: connection pool settings
  feat: optimize detailSuperAdmin API to fix database connection issue
This commit is contained in:
Warunee Tamkoo 2026-01-28 18:26:41 +07:00
commit 07535c9c53
7 changed files with 934 additions and 796 deletions

View file

@ -11,6 +11,8 @@ import { AppDataSource } from "./database/data-source";
import { RegisterRoutes } from "./routes"; import { RegisterRoutes } from "./routes";
import { OrganizationController } from "./controllers/OrganizationController"; import { OrganizationController } from "./controllers/OrganizationController";
import logMiddleware from "./middlewares/logs"; import logMiddleware from "./middlewares/logs";
import { logMemoryStore } from "./utils/LogMemoryStore";
import { orgStructureCache } from "./utils/OrgStructureCache";
import { CommandController } from "./controllers/CommandController"; import { CommandController } from "./controllers/CommandController";
import { ProfileSalaryController } from "./controllers/ProfileSalaryController"; import { ProfileSalaryController } from "./controllers/ProfileSalaryController";
import { DateSerializer } from "./interfaces/date-serializer"; import { DateSerializer } from "./interfaces/date-serializer";
@ -20,6 +22,12 @@ import { initWebSocket } from "./services/webSocket";
async function main() { async function main() {
await AppDataSource.initialize(); await AppDataSource.initialize();
// Initialize LogMemoryStore after database is ready
logMemoryStore.initialize();
// Initialize OrgStructureCache after database is ready
orgStructureCache.initialize();
// Setup custom Date serialization for local timezone // Setup custom Date serialization for local timezone
DateSerializer.setupDateSerialization(); DateSerializer.setupDateSerialization();
@ -93,7 +101,7 @@ async function main() {
}); });
// app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`)); // app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`));
app.listen( const server = app.listen(
APP_PORT, APP_PORT,
APP_HOST, APP_HOST,
() => ( () => (
@ -111,6 +119,46 @@ async function main() {
} }
runMessageQueue(); runMessageQueue();
// Graceful Shutdown
const gracefulShutdown = async (signal: string) => {
console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`);
// Stop accepting new connections
server.close(() => {
console.log("[APP] HTTP server closed");
});
// Force close after timeout
const shutdownTimeout = setTimeout(() => {
console.error("[APP] Forced shutdown after timeout");
process.exit(1);
}, 30000); // 30 seconds timeout
try {
// Close database connections
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
console.log("[APP] Database connections closed");
}
// Destroy LogMemoryStore
logMemoryStore.destroy();
console.log("[APP] LogMemoryStore destroyed");
clearTimeout(shutdownTimeout);
console.log("[APP] Graceful shutdown completed");
process.exit(0);
} catch (error) {
console.error("[APP] Error during shutdown:", error);
clearTimeout(shutdownTimeout);
process.exit(1);
}
};
// Listen for shutdown signals
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
} }
main(); main();

View file

@ -46,10 +46,12 @@ import {
getRoles, getRoles,
addUserRoles, addUserRoles,
} from "../keycloak"; } from "../keycloak";
// import { getPositionCounts, getCounts, getRootCounts } from "../services/OrganizationService";
import { import {
CreatePosMasterHistoryEmployee, CreatePosMasterHistoryEmployee,
CreatePosMasterHistoryOfficer, CreatePosMasterHistoryOfficer,
} from "../services/PositionService"; } from "../services/PositionService";
import { orgStructureCache } from "../utils/OrgStructureCache";
@Route("api/v1/org") @Route("api/v1/org")
@Tags("Organization") @Tags("Organization")
@ -1165,7 +1167,6 @@ export class OrganizationController extends Controller {
async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) { async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) {
const orgRevision = await this.orgRevisionRepository.findOne({ const orgRevision = await this.orgRevisionRepository.findOne({
where: { id: id }, where: { id: id },
relations: ["posMasters"],
}); });
if (!orgRevision) return new HttpSuccess([]); if (!orgRevision) return new HttpSuccess([]);
@ -1175,177 +1176,108 @@ export class OrganizationController extends Controller {
where: { where: {
keycloak: request.user.sub, keycloak: request.user.sub,
}, },
select: ["id"],
}); });
if (profile == null) return new HttpSuccess([]); if (profile == null) return new HttpSuccess([]);
if (!request.user.role.includes("SUPER_ADMIN")) { const posMaster = await this.posMasterRepository.findOne({
if (orgRevision.orgRevisionIsDraft == true && orgRevision.orgRevisionIsCurrent == false) { where:
rootId = orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft
orgRevision?.posMasters?.filter((x) => x.next_holderId == profile.id)[0]?.orgRootId || ? {
null; orgRevisionId: id,
if (!rootId) return new HttpSuccess([]); current_holderId: profile.id,
} else {
rootId =
orgRevision?.posMasters?.filter((x) => x.current_holderId == profile.id)[0]
?.orgRootId || null;
if (!rootId) return new HttpSuccess([]);
}
} }
: {
orgRevisionId: id,
next_holderId: profile.id,
},
});
if (!posMaster) return new HttpSuccess([]);
rootId = posMaster.orgRootId;
} }
// OPTIMIZED: Check cache first
const cachedResponse = await orgStructureCache.get(id, rootId);
if (cachedResponse) {
return new HttpSuccess(cachedResponse);
}
// OPTIMIZED: Get all position counts in ONE query (closed)
// const { orgRootMap, orgChild1Map, orgChild2Map, orgChild3Map, orgChild4Map, rootPosMap } =
// await getPositionCounts(id);
// OPTIMIZED: Fetch orgRoot first, then fetch all child levels in parallel
const orgRootData = await AppDataSource.getRepository(OrgRoot) const orgRootData = await AppDataSource.getRepository(OrgRoot)
.createQueryBuilder("orgRoot") .createQueryBuilder("orgRoot")
.where("orgRoot.orgRevisionId = :id", { id }) .where("orgRoot.orgRevisionId = :id", { id })
.andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", { .andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", {
rootId: rootId, rootId: rootId,
}) })
.select([
"orgRoot.id",
"orgRoot.isDeputy",
"orgRoot.isCommission",
"orgRoot.orgRootName",
"orgRoot.orgRootShortName",
"orgRoot.orgRootCode",
"orgRoot.orgRootOrder",
"orgRoot.orgRootPhoneEx",
"orgRoot.orgRootPhoneIn",
"orgRoot.orgRootFax",
"orgRoot.orgRevisionId",
"orgRoot.orgRootRank",
"orgRoot.orgRootRankSub",
"orgRoot.DEPARTMENT_CODE",
"orgRoot.DIVISION_CODE",
"orgRoot.SECTION_CODE",
"orgRoot.JOB_CODE",
"orgRoot.responsibility",
"orgRoot.ancestorDNA",
])
.orderBy("orgRoot.orgRootOrder", "ASC") .orderBy("orgRoot.orgRootOrder", "ASC")
.getMany(); .getMany();
const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id) || null;
const orgRootIds = orgRootData.map((orgRoot) => orgRoot.id);
// OPTIMIZED: Fetch all child levels in parallel using orgRevisionId
// This is faster than sequential queries that depend on parent IDs
const [orgChild1AllData, orgChild2AllData, orgChild3AllData, orgChild4AllData] =
await Promise.all([
AppDataSource.getRepository(OrgChild1)
.createQueryBuilder("orgChild1")
.where("orgChild1.orgRevisionId = :id", { id })
.orderBy("orgChild1.orgChild1Order", "ASC")
.getMany(),
AppDataSource.getRepository(OrgChild2)
.createQueryBuilder("orgChild2")
.where("orgChild2.orgRevisionId = :id", { id })
.orderBy("orgChild2.orgChild2Order", "ASC")
.getMany(),
AppDataSource.getRepository(OrgChild3)
.createQueryBuilder("orgChild3")
.where("orgChild3.orgRevisionId = :id", { id })
.orderBy("orgChild3.orgChild3Order", "ASC")
.getMany(),
AppDataSource.getRepository(OrgChild4)
.createQueryBuilder("orgChild4")
.where("orgChild4.orgRevisionId = :id", { id })
.orderBy("orgChild4.orgChild4Order", "ASC")
.getMany(),
]);
// Filter child1 data by orgRootIds (maintains backward compatibility)
const orgChild1Data = const orgChild1Data =
orgRootIds && orgRootIds.length > 0 orgRootIds && orgRootIds.length > 0
? await AppDataSource.getRepository(OrgChild1) ? orgChild1AllData.filter((orgChild1) => orgRootIds.includes(orgChild1.orgRootId))
.createQueryBuilder("orgChild1")
.where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds })
.select([
"orgChild1.id",
"orgChild1.isOfficer",
"orgChild1.isInformation",
"orgChild1.orgChild1Name",
"orgChild1.orgChild1ShortName",
"orgChild1.orgChild1Code",
"orgChild1.orgChild1Order",
"orgChild1.orgChild1PhoneEx",
"orgChild1.orgChild1PhoneIn",
"orgChild1.orgChild1Fax",
"orgChild1.orgRootId",
"orgChild1.orgChild1Rank",
"orgChild1.orgChild1RankSub",
"orgChild1.DEPARTMENT_CODE",
"orgChild1.DIVISION_CODE",
"orgChild1.SECTION_CODE",
"orgChild1.JOB_CODE",
"orgChild1.responsibility",
"orgChild1.ancestorDNA",
])
.orderBy("orgChild1.orgChild1Order", "ASC")
.getMany()
: []; : [];
const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; // Build maps for efficient filtering of deeper levels
const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id);
const orgChild2Data = const orgChild2Data =
orgChild1Ids && orgChild1Ids.length > 0 orgChild1Ids && orgChild1Ids.length > 0
? await AppDataSource.getRepository(OrgChild2) ? orgChild2AllData.filter((orgChild2) => orgChild1Ids.includes(orgChild2.orgChild1Id))
.createQueryBuilder("orgChild2")
.where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids })
.select([
"orgChild2.id",
"orgChild2.orgChild2Name",
"orgChild2.orgChild2ShortName",
"orgChild2.orgChild2Code",
"orgChild2.orgChild2Order",
"orgChild2.orgChild2PhoneEx",
"orgChild2.orgChild2PhoneIn",
"orgChild2.orgChild2Fax",
"orgChild2.orgRootId",
"orgChild2.orgChild2Rank",
"orgChild2.orgChild2RankSub",
"orgChild2.DEPARTMENT_CODE",
"orgChild2.DIVISION_CODE",
"orgChild2.SECTION_CODE",
"orgChild2.JOB_CODE",
"orgChild2.orgChild1Id",
"orgChild2.responsibility",
"orgChild2.ancestorDNA",
])
.orderBy("orgChild2.orgChild2Order", "ASC")
.getMany()
: []; : [];
const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id);
const orgChild3Data = const orgChild3Data =
orgChild2Ids && orgChild2Ids.length > 0 orgChild2Ids && orgChild2Ids.length > 0
? await AppDataSource.getRepository(OrgChild3) ? orgChild3AllData.filter((orgChild3) => orgChild2Ids.includes(orgChild3.orgChild2Id))
.createQueryBuilder("orgChild3")
.where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids })
.select([
"orgChild3.id",
"orgChild3.orgChild3Name",
"orgChild3.orgChild3ShortName",
"orgChild3.orgChild3Code",
"orgChild3.orgChild3Order",
"orgChild3.orgChild3PhoneEx",
"orgChild3.orgChild3PhoneIn",
"orgChild3.orgChild3Fax",
"orgChild3.orgRootId",
"orgChild3.orgChild3Rank",
"orgChild3.orgChild3RankSub",
"orgChild3.DEPARTMENT_CODE",
"orgChild3.DIVISION_CODE",
"orgChild3.SECTION_CODE",
"orgChild3.JOB_CODE",
"orgChild3.orgChild2Id",
"orgChild3.responsibility",
"orgChild3.ancestorDNA",
])
.orderBy("orgChild3.orgChild3Order", "ASC")
.getMany()
: []; : [];
const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id);
const orgChild4Data = const orgChild4Data =
orgChild3Ids && orgChild3Ids.length > 0 orgChild3Ids && orgChild3Ids.length > 0
? await AppDataSource.getRepository(OrgChild4) ? orgChild4AllData.filter((orgChild4) => orgChild3Ids.includes(orgChild4.orgChild3Id))
.createQueryBuilder("orgChild4")
.where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids })
.select([
"orgChild4.id",
"orgChild4.orgChild4Name",
"orgChild4.orgChild4ShortName",
"orgChild4.orgChild4Code",
"orgChild4.orgChild4Order",
"orgChild4.orgChild4PhoneEx",
"orgChild4.orgChild4PhoneIn",
"orgChild4.orgChild4Fax",
"orgChild4.orgRootId",
"orgChild4.orgChild4Rank",
"orgChild4.orgChild4RankSub",
"orgChild4.DEPARTMENT_CODE",
"orgChild4.DIVISION_CODE",
"orgChild4.SECTION_CODE",
"orgChild4.JOB_CODE",
"orgChild4.orgChild3Id",
"orgChild4.responsibility",
"orgChild4.ancestorDNA",
])
.orderBy("orgChild4.orgChild4Order", "ASC")
.getMany()
: []; : [];
// const formattedData = orgRootData.map((orgRoot) => { // OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!)
const formattedData = await Promise.all( const formattedData = orgRootData.map((orgRoot) => {
orgRootData.map(async (orgRoot) => { // const rootCounts = getCounts(orgRootMap, orgRoot.id);
// const rootPosCounts = getRootCounts(rootPosMap, orgRoot.id);
return { return {
orgTreeId: orgRoot.id, orgTreeId: orgRoot.id,
orgLevel: 0, orgLevel: 0,
@ -1372,96 +1304,24 @@ export class OrganizationController extends Controller {
responsibility: orgRoot.responsibility, responsibility: orgRoot.responsibility,
labelName: labelName:
orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName, orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName,
totalPosition: await this.posMasterRepository.count({ // totalPosition: rootCounts.totalPosition,
where: { orgRevisionId: orgRoot.orgRevisionId, orgRootId: orgRoot.id }, // totalPositionCurrentUse: rootCounts.totalPositionCurrentUse,
}), // totalPositionCurrentVacant: rootCounts.totalPositionCurrentVacant,
totalPositionCurrentUse: await this.posMasterRepository.count({ // totalPositionNextUse: rootCounts.totalPositionNextUse,
where: { // totalPositionNextVacant: rootCounts.totalPositionNextVacant,
orgRevisionId: orgRoot.orgRevisionId, // totalRootPosition: rootPosCounts.totalRootPosition,
orgRootId: orgRoot.id, // totalRootPositionCurrentUse: rootPosCounts.totalRootPositionCurrentUse,
current_holderId: Not(IsNull()) || Not(""), // totalRootPositionCurrentVacant: rootPosCounts.totalRootPositionCurrentVacant,
}, // totalRootPositionNextUse: rootPosCounts.totalRootPositionNextUse,
}), // totalRootPositionNextVacant: rootPosCounts.totalRootPositionNextVacant,
totalPositionCurrentVacant: await this.posMasterRepository.count({ children: orgChild1Data
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
current_holderId: IsNull() || "",
},
}),
totalPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
next_holderId: IsNull() || "",
},
}),
totalRootPosition: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: IsNull() || "",
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
},
}),
totalRootPositionCurrentUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: IsNull() || "",
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
current_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionCurrentVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: IsNull() || "",
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
current_holderId: IsNull() || "",
},
}),
totalRootPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: IsNull() || "",
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: IsNull() || "",
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
next_holderId: IsNull() || "",
},
}),
children: await Promise.all(
orgChild1Data
.filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id)
.map(async (orgChild1) => ({ .map((orgChild1) => {
// const child1Counts = getCounts(orgChild1Map, orgChild1.id);
// const child1PosKey = `${orgRoot.id}-${orgChild1.id}`;
// const child1PosCounts = getRootCounts(rootPosMap, child1PosKey);
return {
orgTreeId: orgChild1.id, orgTreeId: orgChild1.id,
orgRootId: orgRoot.id, orgRootId: orgRoot.id,
orgLevel: 1, orgLevel: 1,
@ -1495,96 +1355,24 @@ export class OrganizationController extends Controller {
orgChild1.orgChild1Code + orgChild1.orgChild1Code +
" " + " " +
orgChild1.orgChild1ShortName, orgChild1.orgChild1ShortName,
totalPosition: await this.posMasterRepository.count({ // totalPosition: child1Counts.totalPosition,
where: { orgRevisionId: orgRoot.orgRevisionId, orgChild1Id: orgChild1.id }, // totalPositionCurrentUse: child1Counts.totalPositionCurrentUse,
}), // totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant,
totalPositionCurrentUse: await this.posMasterRepository.count({ // totalPositionNextUse: child1Counts.totalPositionNextUse,
where: { // totalPositionNextVacant: child1Counts.totalPositionNextVacant,
orgRevisionId: orgRoot.orgRevisionId, // totalRootPosition: child1PosCounts.totalRootPosition,
orgChild1Id: orgChild1.id, // totalRootPositionCurrentUse: child1PosCounts.totalRootPositionCurrentUse,
current_holderId: Not(IsNull()) || Not(""), // totalRootPositionCurrentVacant: child1PosCounts.totalRootPositionCurrentVacant,
}, // totalRootPositionNextUse: child1PosCounts.totalRootPositionNextUse,
}), // totalRootPositionNextVacant: child1PosCounts.totalRootPositionNextVacant,
totalPositionCurrentVacant: await this.posMasterRepository.count({ children: orgChild2Data
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild1Id: orgChild1.id,
current_holderId: IsNull() || "",
},
}),
totalPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild1Id: orgChild1.id,
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild1Id: orgChild1.id,
next_holderId: IsNull() || "",
},
}),
totalRootPosition: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
},
}),
totalRootPositionCurrentUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
current_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionCurrentVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
current_holderId: IsNull() || "",
},
}),
totalRootPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: IsNull() || "",
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
next_holderId: IsNull() || "",
},
}),
children: await Promise.all(
orgChild2Data
.filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id)
.map(async (orgChild2) => ({ .map((orgChild2) => {
// const child2Counts = getCounts(orgChild2Map, orgChild2.id);
// const child2PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}`;
// const child2PosCounts = getRootCounts(rootPosMap, child2PosKey);
return {
orgTreeId: orgChild2.id, orgTreeId: orgChild2.id,
orgRootId: orgChild1.id, orgRootId: orgChild1.id,
orgLevel: 2, orgLevel: 2,
@ -1617,99 +1405,24 @@ export class OrganizationController extends Controller {
orgChild2.orgChild2Code + orgChild2.orgChild2Code +
" " + " " +
orgChild2.orgChild2ShortName, orgChild2.orgChild2ShortName,
totalPosition: await this.posMasterRepository.count({ // totalPosition: child2Counts.totalPosition,
where: { // totalPositionCurrentUse: child2Counts.totalPositionCurrentUse,
orgRevisionId: orgRoot.orgRevisionId, // totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant,
orgChild2Id: orgChild2.id, // totalPositionNextUse: child2Counts.totalPositionNextUse,
}, // totalPositionNextVacant: child2Counts.totalPositionNextVacant,
}), // totalRootPosition: child2PosCounts.totalRootPosition,
totalPositionCurrentUse: await this.posMasterRepository.count({ // totalRootPositionCurrentUse: child2PosCounts.totalRootPositionCurrentUse,
where: { // totalRootPositionCurrentVacant: child2PosCounts.totalRootPositionCurrentVacant,
orgRevisionId: orgRoot.orgRevisionId, // totalRootPositionNextUse: child2PosCounts.totalRootPositionNextUse,
orgChild2Id: orgChild2.id, // totalRootPositionNextVacant: child2PosCounts.totalRootPositionNextVacant,
current_holderId: Not(IsNull()) || Not(""), children: orgChild3Data
},
}),
totalPositionCurrentVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild2Id: orgChild2.id,
current_holderId: IsNull() || "",
},
}),
totalPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild2Id: orgChild2.id,
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild2Id: orgChild2.id,
next_holderId: IsNull() || "",
},
}),
totalRootPosition: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
},
}),
totalRootPositionCurrentUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
current_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionCurrentVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
current_holderId: IsNull() || "",
},
}),
totalRootPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: IsNull() || "",
orgChild4Id: IsNull() || "",
next_holderId: IsNull() || "",
},
}),
children: await Promise.all(
orgChild3Data
.filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id)
.map(async (orgChild3) => ({ .map((orgChild3) => {
// const child3Counts = getCounts(orgChild3Map, orgChild3.id);
// const child3PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}`;
// const child3PosCounts = getRootCounts(rootPosMap, child3PosKey);
return {
orgTreeId: orgChild3.id, orgTreeId: orgChild3.id,
orgRootId: orgChild2.id, orgRootId: orgChild2.id,
orgLevel: 3, orgLevel: 3,
@ -1743,99 +1456,25 @@ export class OrganizationController extends Controller {
orgChild3.orgChild3Code + orgChild3.orgChild3Code +
" " + " " +
orgChild3.orgChild3ShortName, orgChild3.orgChild3ShortName,
totalPosition: await this.posMasterRepository.count({ // totalPosition: child3Counts.totalPosition,
where: { // totalPositionCurrentUse: child3Counts.totalPositionCurrentUse,
orgRevisionId: orgRoot.orgRevisionId, // totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant,
orgChild3Id: orgChild3.id, // totalPositionNextUse: child3Counts.totalPositionNextUse,
}, // totalPositionNextVacant: child3Counts.totalPositionNextVacant,
}), // totalRootPosition: child3PosCounts.totalRootPosition,
totalPositionCurrentUse: await this.posMasterRepository.count({ // totalRootPositionCurrentUse: child3PosCounts.totalRootPositionCurrentUse,
where: { // totalRootPositionCurrentVacant:
orgRevisionId: orgRoot.orgRevisionId, // child3PosCounts.totalRootPositionCurrentVacant,
orgChild3Id: orgChild3.id, // totalRootPositionNextUse: child3PosCounts.totalRootPositionNextUse,
current_holderId: Not(IsNull()) || Not(""), // totalRootPositionNextVacant: child3PosCounts.totalRootPositionNextVacant,
}, children: orgChild4Data
}),
totalPositionCurrentVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild3Id: orgChild3.id,
current_holderId: IsNull() || "",
},
}),
totalPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild3Id: orgChild3.id,
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild3Id: orgChild3.id,
next_holderId: IsNull() || "",
},
}),
totalRootPosition: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: IsNull() || "",
},
}),
totalRootPositionCurrentUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: IsNull() || "",
current_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionCurrentVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: IsNull() || "",
current_holderId: IsNull() || "",
},
}),
totalRootPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: IsNull() || "",
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: IsNull() || "",
next_holderId: IsNull() || "",
},
}),
children: await Promise.all(
orgChild4Data
.filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id)
.map(async (orgChild4) => ({ .map((orgChild4) => {
// const child4Counts = getCounts(orgChild4Map, orgChild4.id);
// const child4PosKey = `${orgRoot.id}-${orgChild1.id}-${orgChild2.id}-${orgChild3.id}-${orgChild4.id}`;
// const child4PosCounts = getRootCounts(rootPosMap, child4PosKey);
return {
orgTreeId: orgChild4.id, orgTreeId: orgChild4.id,
orgRootId: orgChild3.id, orgRootId: orgChild3.id,
orgLevel: 4, orgLevel: 4,
@ -1870,110 +1509,33 @@ export class OrganizationController extends Controller {
orgChild4.orgChild4Code + orgChild4.orgChild4Code +
" " + " " +
orgChild4.orgChild4ShortName, orgChild4.orgChild4ShortName,
totalPosition: await this.posMasterRepository.count({ // totalPosition: child4Counts.totalPosition,
where: { // totalPositionCurrentUse: child4Counts.totalPositionCurrentUse,
orgRevisionId: orgRoot.orgRevisionId, // totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant,
orgChild4Id: orgChild4.id, // totalPositionNextUse: child4Counts.totalPositionNextUse,
}, // totalPositionNextVacant: child4Counts.totalPositionNextVacant,
}), // totalRootPosition: child4PosCounts.totalRootPosition,
totalPositionCurrentUse: await this.posMasterRepository.count({ // totalRootPositionCurrentUse:
where: { // child4PosCounts.totalRootPositionCurrentUse,
orgRevisionId: orgRoot.orgRevisionId, // totalRootPositionCurrentVacant:
orgChild4Id: orgChild4.id, // child4PosCounts.totalRootPositionCurrentVacant,
current_holderId: Not(IsNull()) || Not(""), // totalRootPositionNextUse: child4PosCounts.totalRootPositionNextUse,
}, // totalRootPositionNextVacant:
}), // child4PosCounts.totalRootPositionNextVacant,
totalPositionCurrentVacant: await this.posMasterRepository.count({ children: [],
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild4Id: orgChild4.id,
current_holderId: IsNull() || "",
},
}),
totalPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild4Id: orgChild4.id,
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalPositionNextVacant: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgChild4Id: orgChild4.id,
next_holderId: IsNull() || "",
},
}),
totalRootPosition: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: orgChild4.id,
},
}),
totalRootPositionCurrentUse: await this.posMasterRepository.count(
{
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: orgChild4.id,
current_holderId: Not(IsNull()) || Not(""),
},
},
),
totalRootPositionCurrentVacant:
await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: orgChild4.id,
current_holderId: IsNull() || "",
},
}),
totalRootPositionNextUse: await this.posMasterRepository.count({
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: orgChild4.id,
next_holderId: Not(IsNull()) || Not(""),
},
}),
totalRootPositionNextVacant: await this.posMasterRepository.count(
{
where: {
orgRevisionId: orgRoot.orgRevisionId,
orgRootId: orgRoot.id,
orgChild1Id: orgChild1.id,
orgChild2Id: orgChild2.id,
orgChild3Id: orgChild3.id,
orgChild4Id: orgChild4.id,
next_holderId: IsNull() || "",
},
},
),
})),
),
})),
),
})),
),
})),
),
}; };
}), }),
); };
}),
};
}),
};
}),
};
});
// OPTIMIZED: Cache the result
await orgStructureCache.set(id, rootId, formattedData);
return new HttpSuccess(formattedData); return new HttpSuccess(formattedData);
} }

View file

@ -56,6 +56,16 @@ export const AppDataSource = new DataSource({
: ["dist/migration/**/*{.ts,.js}"], : ["dist/migration/**/*{.ts,.js}"],
subscribers: [], subscribers: [],
logger: new MyCustomLogger(), logger: new MyCustomLogger(),
// Connection pool settings to prevent connection exhaustion
extra: {
connectionLimit: +(process.env.DB_CONNECTION_LIMIT || 50),
maxIdle: +(process.env.DB_MAX_IDLE || 10),
idleTimeout: +(process.env.DB_IDLE_TIMEOUT || 60000),
timezone: "+07:00", // Bangkok timezone (UTC+7)
},
// TypeORM pool settings
poolSize: +(process.env.DB_POOL_SIZE || 10),
maxQueryExecutionTime: +(process.env.DB_MAX_QUERY_TIME || 3000),
}); });
// export default database; // export default database;

View file

@ -1,9 +1,6 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { Client } from "@elastic/elasticsearch"; import { Client } from "@elastic/elasticsearch";
import { AppDataSource } from "../database/data-source"; import { logMemoryStore } from "../utils/LogMemoryStore";
import { PosMaster } from "../entities/PosMaster";
import { OrgRevision } from "../entities/OrgRevision";
import { Profile } from "../entities/Profile";
if (!process.env.ELASTICSEARCH_INDEX) { if (!process.env.ELASTICSEARCH_INDEX) {
throw new Error("Require ELASTICSEARCH_INDEX to store log."); throw new Error("Require ELASTICSEARCH_INDEX to store log.");
@ -27,9 +24,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.url.startsWith("/api/")) return next(); if (!req.url.startsWith("/api/")) return next();
let data: any; let data: any;
const repoPosmaster = AppDataSource.getRepository(PosMaster);
const repoProfile = AppDataSource.getRepository(Profile);
const repoRevision = AppDataSource.getRepository(OrgRevision);
const originalJson = res.json; const originalJson = res.json;
@ -42,13 +36,6 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
req.app.locals.logData = {}; req.app.locals.logData = {};
const revision = await repoRevision.findOne({
where: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
});
res.on("finish", async () => { res.on("finish", async () => {
try { try {
if (!req.url.startsWith("/api/")) return; if (!req.url.startsWith("/api/")) return;
@ -79,16 +66,15 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
if (req.url.startsWith("/api/v1/org/keycloak/")) system = "registry"; if (req.url.startsWith("/api/v1/org/keycloak/")) system = "registry";
const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4; const level = LOG_LEVEL_MAP[process.env.LOG_LEVEL ?? "debug"] || 4;
const profileByKeycloak = await repoProfile.findOne({
where: { keycloak: req.app.locals.logData.userId }, // Get profile from cache
}); const profileByKeycloak = await logMemoryStore.getProfileByKeycloak(
const rootId = await repoPosmaster.findOne({ req.app.locals.logData.userId,
where: { );
current_holderId: profileByKeycloak?.id,
orgRevisionId: revision?.id, // Get rootId from cache
}, const rootId = await logMemoryStore.getRootIdByProfileId(profileByKeycloak?.id);
select: ["orgRootId"], // console.log("ancestorDNA:", rootId);
});
if (level === 1 && res.statusCode < 500) return; if (level === 1 && res.statusCode < 500) return;
if (level === 2 && res.statusCode < 400) return; if (level === 2 && res.statusCode < 400) return;
@ -96,7 +82,7 @@ async function logMiddleware(req: Request, res: Response, next: NextFunction) {
const obj = { const obj = {
logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info", logType: res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warning" : "info",
ip: req.ip, ip: req.ip,
rootId: rootId?.orgRootId ?? null, rootId: rootId ?? null,
systemName: system, systemName: system,
startTimeStamp: timestamp, startTimeStamp: timestamp,
endTimeStamp: new Date().toISOString(), endTimeStamp: new Date().toISOString(),

View file

@ -0,0 +1,278 @@
import { AppDataSource } from "../database/data-source";
import { PosMaster } from "../entities/PosMaster";
// Helper function to get aggregated position counts
export async function getPositionCounts(orgRevisionId: string) {
// Query all posMaster data for this revision with aggregation
const rawData = await AppDataSource.getRepository(PosMaster)
.createQueryBuilder("pos")
.select([
"pos.orgRootId",
"pos.orgChild1Id",
"pos.orgChild2Id",
"pos.orgChild3Id",
"pos.orgChild4Id",
"pos.current_holderId",
"pos.next_holderId",
])
.where("pos.orgRevisionId = :orgRevisionId", { orgRevisionId })
.getMany();
// Helper to check if value is null or empty string
const isNull = (val: any) => val === null || val === "";
// Build maps for each level
const orgRootMap = new Map<string, any>();
const orgChild1Map = new Map<string, any>();
const orgChild2Map = new Map<string, any>();
const orgChild3Map = new Map<string, any>();
const orgChild4Map = new Map<string, any>();
// Nested maps for root positions (positions at specific levels)
const rootPosMap = new Map<string, any>();
for (const pos of rawData) {
const orgRootId = pos.orgRootId || "NULL";
const orgChild1Id = pos.orgChild1Id || "NULL";
const orgChild2Id = pos.orgChild2Id || "NULL";
const orgChild3Id = pos.orgChild3Id || "NULL";
const orgChild4Id = pos.orgChild4Id || "NULL";
// Level 0 (orgRoot) counts
if (!orgRootMap.has(orgRootId)) {
orgRootMap.set(orgRootId, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const rootCounts = orgRootMap.get(orgRootId);
rootCounts.totalPosition++;
if (!isNull(pos.current_holderId)) rootCounts.totalPositionCurrentUse++;
else rootCounts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) rootCounts.totalPositionNextUse++;
else rootCounts.totalPositionNextVacant++;
// Level 1 (orgChild1) counts
if (!isNull(pos.orgChild1Id)) {
if (!orgChild1Map.has(orgChild1Id)) {
orgChild1Map.set(orgChild1Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child1Counts = orgChild1Map.get(orgChild1Id);
child1Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child1Counts.totalPositionCurrentUse++;
else child1Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child1Counts.totalPositionNextUse++;
else child1Counts.totalPositionNextVacant++;
}
// Level 2 (orgChild2) counts
if (!isNull(pos.orgChild2Id)) {
if (!orgChild2Map.has(orgChild2Id)) {
orgChild2Map.set(orgChild2Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child2Counts = orgChild2Map.get(orgChild2Id);
child2Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child2Counts.totalPositionCurrentUse++;
else child2Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child2Counts.totalPositionNextUse++;
else child2Counts.totalPositionNextVacant++;
}
// Level 3 (orgChild3) counts
if (!isNull(pos.orgChild3Id)) {
if (!orgChild3Map.has(orgChild3Id)) {
orgChild3Map.set(orgChild3Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child3Counts = orgChild3Map.get(orgChild3Id);
child3Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child3Counts.totalPositionCurrentUse++;
else child3Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child3Counts.totalPositionNextUse++;
else child3Counts.totalPositionNextVacant++;
}
// Level 4 (orgChild4) counts
if (!isNull(pos.orgChild4Id)) {
if (!orgChild4Map.has(orgChild4Id)) {
orgChild4Map.set(orgChild4Id, {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
});
}
const child4Counts = orgChild4Map.get(orgChild4Id);
child4Counts.totalPosition++;
if (!isNull(pos.current_holderId)) child4Counts.totalPositionCurrentUse++;
else child4Counts.totalPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child4Counts.totalPositionNextUse++;
else child4Counts.totalPositionNextVacant++;
}
// Root position counts (positions at specific hierarchy levels)
// For orgRoot level (all children are null)
const rootLevelKey = orgRootId;
if (!rootPosMap.has(rootLevelKey)) {
rootPosMap.set(rootLevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild1Id) && isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
const rootLevelCounts = rootPosMap.get(rootLevelKey);
rootLevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) rootLevelCounts.totalRootPositionCurrentUse++;
else rootLevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) rootLevelCounts.totalRootPositionNextUse++;
else rootLevelCounts.totalRootPositionNextVacant++;
}
// For orgChild1 level
if (!isNull(pos.orgChild1Id)) {
const child1LevelKey = `${orgRootId}-${pos.orgChild1Id}`;
if (!rootPosMap.has(child1LevelKey)) {
rootPosMap.set(child1LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild2Id) && isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
const child1LevelCounts = rootPosMap.get(child1LevelKey);
child1LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child1LevelCounts.totalRootPositionCurrentUse++;
else child1LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child1LevelCounts.totalRootPositionNextUse++;
else child1LevelCounts.totalRootPositionNextVacant++;
}
}
// For orgChild2 level
if (!isNull(pos.orgChild2Id)) {
const child2LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}`;
if (!rootPosMap.has(child2LevelKey)) {
rootPosMap.set(child2LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild3Id) && isNull(pos.orgChild4Id)) {
const child2LevelCounts = rootPosMap.get(child2LevelKey);
child2LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child2LevelCounts.totalRootPositionCurrentUse++;
else child2LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child2LevelCounts.totalRootPositionNextUse++;
else child2LevelCounts.totalRootPositionNextVacant++;
}
}
// For orgChild3 level
if (!isNull(pos.orgChild3Id)) {
const child3LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}`;
if (!rootPosMap.has(child3LevelKey)) {
rootPosMap.set(child3LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
if (isNull(pos.orgChild4Id)) {
const child3LevelCounts = rootPosMap.get(child3LevelKey);
child3LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child3LevelCounts.totalRootPositionCurrentUse++;
else child3LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child3LevelCounts.totalRootPositionNextUse++;
else child3LevelCounts.totalRootPositionNextVacant++;
}
}
// For orgChild4 level
if (!isNull(pos.orgChild4Id)) {
const child4LevelKey = `${orgRootId}-${pos.orgChild1Id}-${pos.orgChild2Id}-${orgChild3Id}-${pos.orgChild4Id}`;
if (!rootPosMap.has(child4LevelKey)) {
rootPosMap.set(child4LevelKey, {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
});
}
const child4LevelCounts = rootPosMap.get(child4LevelKey);
child4LevelCounts.totalRootPosition++;
if (!isNull(pos.current_holderId)) child4LevelCounts.totalRootPositionCurrentUse++;
else child4LevelCounts.totalRootPositionCurrentVacant++;
if (!isNull(pos.next_holderId)) child4LevelCounts.totalRootPositionNextUse++;
else child4LevelCounts.totalRootPositionNextVacant++;
}
}
return {
orgRootMap,
orgChild1Map,
orgChild2Map,
orgChild3Map,
orgChild4Map,
rootPosMap,
};
}
// Helper function to get counts from maps with defaults
export function getCounts(map: Map<string, any>, key: string) {
return (
map.get(key) || {
totalPosition: 0,
totalPositionCurrentUse: 0,
totalPositionCurrentVacant: 0,
totalPositionNextUse: 0,
totalPositionNextVacant: 0,
}
);
}
// Helper function to get root position counts
export function getRootCounts(map: Map<string, any>, key: string) {
return (
map.get(key) || {
totalRootPosition: 0,
totalRootPositionCurrentUse: 0,
totalRootPositionCurrentVacant: 0,
totalRootPositionNextUse: 0,
totalRootPositionNextVacant: 0,
}
);
}

158
src/utils/LogMemoryStore.ts Normal file
View file

@ -0,0 +1,158 @@
import { AppDataSource } from "../database/data-source";
import { OrgRevision } from "../entities/OrgRevision";
import { Profile } from "../entities/Profile";
import { PosMaster } from "../entities/PosMaster";
interface LogCacheData {
currentRevision: OrgRevision | null;
profileCache: Map<string, Profile>; // keycloak → Profile
rootIdCache: Map<string, string>; // profileId → rootId
updatedAt: Date;
}
class LogMemoryStore {
private cache: LogCacheData = {
currentRevision: null,
profileCache: new Map(),
rootIdCache: new Map(),
updatedAt: new Date(),
};
private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
private isRefreshing = false;
private isInitialized = false;
private refreshTimer: NodeJS.Timeout | null = null;
constructor() {
// ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready
}
// เริ่มต้น cache หลังจาก TypeORM initialize เสร็จ
initialize() {
if (this.isInitialized) {
console.log("[LogMemoryStore] Already initialized");
return;
}
this.isInitialized = true;
this.refreshCache();
this.refreshTimer = setInterval(() => {
this.refreshCache();
}, this.REFRESH_INTERVAL);
console.log(
"[LogMemoryStore] Initialized with",
this.REFRESH_INTERVAL / 1000,
"second refresh interval",
);
}
private async refreshCache() {
if (this.isRefreshing) {
console.log("[LogMemoryStore] Already refreshing, skipping...");
return;
}
this.isRefreshing = true;
try {
// Refresh revision cache
const repoRevision = AppDataSource.getRepository(OrgRevision);
const revision = await repoRevision.findOne({
where: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
});
this.cache.currentRevision = revision;
// Clear on-demand caches (they will be rebuilt as needed)
this.cache.profileCache.clear();
this.cache.rootIdCache.clear();
this.cache.updatedAt = new Date();
console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString());
} catch (error) {
console.error("[LogMemoryStore] Error refreshing cache:", error);
} finally {
this.isRefreshing = false;
}
}
getCurrentRevision(): OrgRevision | null {
return this.cache.currentRevision;
}
getLastUpdateTime(): Date {
return this.cache.updatedAt;
}
/**
* Get Profile by keycloak ID with caching
*/
async getProfileByKeycloak(keycloak: string): Promise<Profile | null> {
// Check cache first
if (this.cache.profileCache.has(keycloak)) {
return this.cache.profileCache.get(keycloak)!;
}
// Fetch from database
const repoProfile = AppDataSource.getRepository(Profile);
const profile = await repoProfile.findOne({
where: { keycloak },
});
// Cache the result
if (profile) {
this.cache.profileCache.set(keycloak, profile);
}
return profile;
}
/**
* Get RootId by profileId with caching
*/
async getRootIdByProfileId(profileId: string | undefined): Promise<string | null> {
if (!profileId) return null;
// Check cache first
if (this.cache.rootIdCache.has(profileId)) {
return this.cache.rootIdCache.get(profileId)!;
}
// Fetch from database
const repoPosmaster = AppDataSource.getRepository(PosMaster);
const revision = this.getCurrentRevision();
//
const posMaster = await repoPosmaster.findOne({
where: {
current_holderId: profileId,
orgRevisionId: revision?.id,
},
relations: ["orgRoot"],
select: {
orgRoot: {
ancestorDNA: true,
},
},
});
const rootId = posMaster?.orgRoot?.ancestorDNA ?? null;
// Cache the result
if (rootId) {
this.cache.rootIdCache.set(profileId, rootId);
}
return rootId;
}
// สำหรับ shutdown
destroy() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
}
export const logMemoryStore = new LogMemoryStore();

View file

@ -0,0 +1,96 @@
interface CacheEntry {
data: any;
cachedAt: Date;
}
class OrgStructureCache {
private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_TTL = 10 * 60 * 1000; // 10 minutes
private isInitialized = false;
private cleanupTimer: NodeJS.Timeout | null = null;
constructor() {
// Don't auto-initialize - wait for AppDataSource to be ready
}
initialize() {
if (this.isInitialized) return;
this.isInitialized = true;
// Cleanup expired entries every 10 minutes
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.CACHE_TTL);
console.log("[OrgStructureCache] Initialized");
}
private generateKey(revisionId: string, rootId?: string): string {
return `org-structure-${revisionId}-${rootId || "all"}`;
}
private cleanup() {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
const age = now - entry.cachedAt.getTime();
if (age > this.CACHE_TTL) {
this.cache.delete(key);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`[OrgStructureCache] Cleaned ${cleaned} expired entries`);
}
}
async get(revisionId: string, rootId?: string): Promise<any | null> {
const key = this.generateKey(revisionId, rootId);
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Check if expired
const age = Date.now() - entry.cachedAt.getTime();
if (age > this.CACHE_TTL) {
this.cache.delete(key);
return null;
}
console.log(`[OrgStructureCache] HIT: ${key}`);
return entry.data;
}
async set(revisionId: string, rootId: string | undefined, data: any): Promise<void> {
const key = this.generateKey(revisionId, rootId);
this.cache.set(key, {
data,
cachedAt: new Date(),
});
console.log(`[OrgStructureCache] SET: ${key}`);
}
invalidate(revisionId: string): void {
// Invalidate all entries for this revision
for (const key of this.cache.keys()) {
if (key.startsWith(`org-structure-${revisionId}`)) {
this.cache.delete(key);
}
}
console.log(`[OrgStructureCache] INVALIDATED: ${revisionId}`);
}
destroy() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
this.cache.clear();
}
}
export const orgStructureCache = new OrgStructureCache();