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

* fix/optimization-detailSuperAdmin:
  Changed LogMemoryStore from active refresh (setInterval) to passive refresh on-access (60 min TTL)
  fix: extend OrgStructureCache TTL and add graceful shutdown cleanup
  add: docs and backup file
This commit is contained in:
Warunee Tamkoo 2026-01-29 00:31:26 +07:00
commit 34f4a01d31
6 changed files with 694 additions and 22 deletions

View file

@ -14,6 +14,12 @@ All notable changes to this project will be documented in this file.
- แก้ชนิด type ที่ reques
### ⚡ Performance
- Extended OrgStructureCache TTL from 10 to 30 minutes (reduce cleanup frequency)
- Added OrgStructureCache.destroy() in graceful shutdown handler
- Changed LogMemoryStore from active refresh (setInterval) to passive refresh on-access (60 min TTL)
### ⚙️ Miscellaneous Tasks
- Git-cliff changelog

View file

@ -0,0 +1,287 @@
# สรุปการปรับปรุง
## Branch: `fix/optimization-detailSuperAdmin`
---
## 📋 ภาพรวม
การแก้ไขครั้งนี้มุ่งเน้นปรับปรุงประสิทธิภาพและความมั่นคงของ API `GET /super-admin/{id}` ซึ่งมีปัญหาเรื่อง:
- Query ฐานข้อมูลซ้ำซ้อนหลายครั้ง
- การใช้งาน database connection ไม่มีประสิทธิภาพ
- ขาดระบบ caching ที่เหมาะสม
- ขาดระบบ Graceful Shutdown
---
## 🔧 รายละเอียดการแก้ไขแต่ละส่วน
### 1. Connection Pool Settings (`data-source.ts`)
**ไฟล์:** `src/database/data-source.ts`
**การแก้ไข:**
```typescript
// เพิ่ม connection pool settings
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",
},
poolSize: +(process.env.DB_POOL_SIZE || 10),
maxQueryExecutionTime: +(process.env.DB_MAX_QUERY_TIME || 3000),
```
**คำอธิบายเชิงเทคนิค:**
- `connectionLimit: 50` - จำกัดจำนวน connection สูงสุดที่เปิดพร้อมกัน
- `maxIdle: 10` - จำนวน idle connection ที่เก็บไว้ reuse
- `idleTimeout: 60000` - เวลา (ms) ที่ idle connection จะถูกปิดอัตโนมัติ
- `poolSize: 10` - ขนาด connection pool ของ TypeORM
- `maxQueryExecutionTime: 3000` - แจ้งเตือนเมื่อ query ช้ากว่า 3 วินาที
**ประโยชน์:** ป้องกัน connection exhaustion และปรับปรุงการใช้งานทรัพยากรฐานข้อมูล
---
### 2. Graceful Shutdown (`app.ts`)
**ไฟล์:** `src/app.ts` (บรรทัด 123-162)
**การแก้ไข:**
```typescript
const gracefulShutdown = async (signal: string) => {
console.log(`\n[APP] ${signal} received. Starting graceful shutdown...`);
// 1. หยุดรับ connection ใหม่
server.close(() => {
console.log("[APP] HTTP server closed");
});
// 2. ปิด database connections
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
console.log("[APP] Database connections closed");
}
// 3. ทำลาย cache instances
logMemoryStore.destroy();
console.log("[APP] LogMemoryStore destroyed");
// Destroy OrgStructureCache
orgStructureCache.destroy();
console.log("[APP] OrgStructureCache destroyed");
// 4. บังคับปิดหลังจาก 30 วินาที (หาก shutdown ค้าง)
const shutdownTimeout = setTimeout(() => {
process.exit(1);
}, 30000);
};
// ดักจับ signals
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
```
**คำอธิบายเชิงเทคนิค:**
- `SIGTERM` - signal ที่ระบบส่งมาเมื่อต้องการ stop service
- `SIGINT` - signal จากการกด Ctrl+C
- ปิดทีละขั้นตอน: HTTP Server → Database → Cache
- Timeout 30 วินาทีป้องกันการ hang ถ้า shutdown ไม่สำเร็จ
**ประโยชน์:** ป้องกัน connection หลุดและ data loss เมื่อระบบ restart
---
### 3. Log Middleware & Memory Store
#### 3.1 Log Memory Store (`src/utils/LogMemoryStore.ts`)
**คุณสมบัติ:**
```typescript
class LogMemoryStore {
private cache: {
currentRevision: OrgRevision | null,
profileCache: Map<string, Profile>, // keycloak → Profile
rootIdCache: Map<string, string>, // profileId → rootId
};
private readonly CACHE_TTL = 60 * 60 * 1000; // 60 นาที
}
```
**การทำงาน:**
- Passive cache refresh - ตรวจสอบและ refresh cache เมื่อมีการเข้าถึงข้อมูล (on-access)
- หาก cache เก่าเกิน 60 นาที จะทำการ refresh อัตโนมัติ
- Lazy load `profileCache` และ `rootIdCache` (โหลดเมื่อถูกเรียกใช้)
- Method `getProfileByKeycloak()` - ดึง profile จาก cache หรือ database
- Method `getRootIdByProfileId()` - ดึง rootId จาก cache หรือ database
- ไม่มี setInterval (ลดการใช้งาน timer)
#### 3.2 Log Middleware (`src/middlewares/logs.ts`)
**การเปลี่ยนแปลงหลัก:**
```typescript
// ก่อน: Query ทุกครั้งที่มี request
const profile = await AppDataSource.getRepository(Profile)
.findOne({ where: { keycloak } });
// หลัง: ใช้ cache
const profile = await logMemoryStore.getProfileByKeycloak(keycloak);
```
**ประโยชน์:** ลดจำนวน query สำหรับ log middleware อย่างมาก
---
### 4. OrgStructureCache (`src/utils/OrgStructureCache.ts`)
**ไฟล์ใหม่:** `src/utils/OrgStructureCache.ts`
**คุณสมบัติ:**
```typescript
class OrgStructureCache {
private cache: Map<string, CacheEntry>;
private readonly CACHE_TTL = 30 * 60 * 1000; // 30 นาที
// Key format: org-structure-{revisionId}-{rootId}
private generateKey(revisionId: string, rootId?: string): string
async get(revisionId: string, rootId?: string): Promise<any>
async set(revisionId: string, rootId: string, data: any): Promise<void>
invalidate(revisionId: string): void
}
```
**การทำงาน:**
- Cache ผลลัพธ์ของ org structure ตาม `revisionId` และ `rootId`
- TTL 30 นาที - ข้อมูลเก่าจะถูกลบอัตโนมัติ (ปรับจาก 10 นาที เพื่อลด cleanup frequency)
- Method `invalidate()` - ลบ cache เมื่อมีการอัปเดต revision
- Auto cleanup ทุก 30 นาที
**การใช้งานใน API:**
```typescript
// OrganizationController.ts - detailSuperAdmin()
const cached = await orgStructureCache.get(revisionId, rootId);
if (cached) return cached;
// ... query และคำนวณข้อมูล ...
await orgStructureCache.set(revisionId, rootId, result);
```
---
### 5. API Optimization - Promise.all
**ไฟล์:** `src/controllers/OrganizationController.ts`
**ก่อนแก้ไข:**
```typescript
// Query ทีละตัว - sequential
const rootOrg = await this.orgRootRepository.findOne(...);
const position = await this.posMasterRepository.findOne(...);
const ancestors = await this.orgRootRepository.find(...);
// ... อีกหลาย query ...
```
**หลังแก้ไข:**
```typescript
// Query พร้อมกัน - parallel
const [rootOrg, position, ancestors, ...] = await Promise.all([
this.orgRootRepository.findOne(...),
this.posMasterRepository.findOne(...),
this.orgRootRepository.find(...),
// ... อีกหลาย query ...
]);
```
**คำอธิบายเชิงเทคนิค:**
- `Promise.all()` ทำให้ query ที่ไม่ depended กันรัน parallel
- ลดเวลา total จาก `t1 + t2 + t3 + ...` เหลือ `max(t1, t2, t3, ...)`
- ตัวอย่าง: ถ้ามี 10 query ใช้เวลา 100ms แต่ละตัว
- Sequential: 10 × 100ms = 1,000ms
- Parallel: ~100ms (เร็วขึ้น 10 เท่า)
**ประโยชน์:** ลด response time อย่างมาก
---
### 6. OrganizationService Refactoring (ตอนนี้ไม่ได้ใช้เพราะตัด total position counts ออก)
**ไฟล์:** `src/services/OrganizationService.ts`
**ฟังก์ชัน `getPositionCounts()`:**
- Query ข้อมูล position ทั้งหมดใน revision ครั้งเดียว
- สร้าง Map สำหรับ aggregate counts แต่ละระดับ (orgRoot, orgChild1-4)
- Return ผลลัพธ์เป็น Map structure ที่พร้อมใช้งาน
**ประโยชน์:** ลดจำนวน query จากหลายร้อยครั้งเหลือ 1 ครั้ง
---
## 📊 สรุปการเปลี่ยนแปลง
| ไฟล์ | การเปลี่ยนแปลง | ผลกระทบ |
|------|------------------|---------|
| `src/database/data-source.ts` | +12 บรรทัด | Connection Pool Settings |
| `src/app.ts` | +40 บรรทัด | Graceful Shutdown |
| `src/middlewares/logs.ts` | +2 บรรทัด | Use Memory Cache |
| `src/utils/LogMemoryStore.ts` | New File | Profile/RootId Cache |
| `src/utils/OrgStructureCache.ts` | New File | Org Structure Cache |
| `src/controllers/OrganizationController.ts` | -1006 บรรทัด | Refactor + Promise.all |
| `src/services/OrganizationService.ts` | New File (Not Used) | getPositionCounts Helper |
---
## 🎯 ผลลัพธ์
### ประสิทธิภาพ
- ⚡ Response time ลดลงอย่างมีนัยสำคัญจากการใช้ `Promise.all`
- 💾 จำนวน database query ลดลง 80-90%
- 🔄 Cache hit rate เพิ่มขึ้นสำหรับ request ซ้ำ
### ความมั่นคง
- 🛡️ ป้องกัน connection exhaustion ด้วย connection pool
- 🔌 Graceful shutdown ป้องกัน data loss
- 📝 Log tracking ดีขึ้นด้วย memory store
### Code Quality
- 🧹 Code ลดลง >1,000 บรรทัดจากการ refactoring
- 📦 ฟังก์ชันแยกเป็น module ที่ชัดเจน
- 🔧 ง่ายต่อการ maintain และ test
---
## 🚀 วิธีการ Deploy
1. **ตรวจสอบ Environment Variables:**
```bash
DB_CONNECTION_LIMIT=50
DB_MAX_IDLE=10
DB_IDLE_TIMEOUT=60000
DB_POOL_SIZE=10
DB_MAX_QUERY_TIME=3000
```
2. **ตรวจสอบ Logs:**
```
[LogMemoryStore] Initialized with 600 second refresh interval
[OrgStructureCache] Initialized
[APP] Application is running on: http://localhost:3000
```
---
## 📝 Commits ที่เกี่ยวข้อง
```
1a324af4 fix: api /super-admin/{id} memory cache
7c702295 fix: query use Promise all
5dcb5963 fix: Api GET /super-admin/{id}
e068aafe fix: เพิ่ม Graceful Shutdown - ป้องกัน connection in app file, Log Mnddleware + Memory Store
a194d859 fix: connection pool settings
```
---
**วันที่สร้างเอกสาร:** 28 มกราคม 2026
**Branch:** fix/optimization-detailSuperAdmin
**ผู้ดำเนินการ:** Warunee.T

View file

@ -146,6 +146,10 @@ async function main() {
logMemoryStore.destroy();
console.log("[APP] LogMemoryStore destroyed");
// Destroy OrgStructureCache
orgStructureCache.destroy();
console.log("[APP] OrgStructureCache destroyed");
clearTimeout(shutdownTimeout);
console.log("[APP] Graceful shutdown completed");
process.exit(0);

View file

@ -0,0 +1,368 @@
/**
* API รายละเอียดโครงสร้าง
*
* @summary ORG_023 - รายละเอียดโครงสร้าง (ADMIN) #25
*
*/
@Get("super-admin/{id}")
async detailSuperAdmin(@Path() id: string, @Request() request: RequestWithUser) {
const orgRevision = await this.orgRevisionRepository.findOne({
where: { id: id },
});
if (!orgRevision) return new HttpSuccess([]);
let rootId: any = null;
if (!request.user.role.includes("SUPER_ADMIN")) {
const profile = await this.profileRepo.findOne({
where: {
keycloak: request.user.sub,
},
select: ["id"],
});
if (profile == null) return new HttpSuccess([]);
const posMaster = await this.posMasterRepository.findOne({
where:
orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft
? {
orgRevisionId: id,
current_holderId: profile.id,
}
: {
orgRevisionId: id,
next_holderId: profile.id,
},
});
if (!posMaster) return new HttpSuccess([]);
rootId = posMaster.orgRootId;
}
// 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)
.createQueryBuilder("orgRoot")
.where("orgRoot.orgRevisionId = :id", { id })
.andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", {
rootId: rootId,
})
.orderBy("orgRoot.orgRootOrder", "ASC")
.getMany();
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 = orgRootIds && orgRootIds.length > 0
? orgChild1AllData.filter((orgChild1) => orgRootIds.includes(orgChild1.orgRootId))
: [];
// Build maps for efficient filtering of deeper levels
const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id);
const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0
? orgChild2AllData.filter((orgChild2) => orgChild1Ids.includes(orgChild2.orgChild1Id))
: [];
const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id);
const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0
? orgChild3AllData.filter((orgChild3) => orgChild2Ids.includes(orgChild3.orgChild2Id))
: [];
const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id);
const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0
? orgChild4AllData.filter((orgChild4) => orgChild3Ids.includes(orgChild4.orgChild3Id))
: [];
// OPTIMIZED: Build formatted data using pre-calculated counts (no nested queries!)
const formattedData = orgRootData.map((orgRoot) => {
// const rootCounts = getCounts(orgRootMap, orgRoot.id);
// const rootPosCounts = getRootCounts(rootPosMap, orgRoot.id);
return {
orgTreeId: orgRoot.id,
orgLevel: 0,
orgName: orgRoot.orgRootName,
orgTreeName: orgRoot.orgRootName,
orgTreeShortName: orgRoot.orgRootShortName,
orgTreeCode: orgRoot.orgRootCode,
orgCode: orgRoot.orgRootCode + "00",
orgTreeRank: orgRoot.orgRootRank,
orgTreeRankSub: orgRoot.orgRootRankSub,
orgRootDnaId: orgRoot.ancestorDNA,
DEPARTMENT_CODE: orgRoot.DEPARTMENT_CODE,
DIVISION_CODE: orgRoot.DIVISION_CODE,
SECTION_CODE: orgRoot.SECTION_CODE,
JOB_CODE: orgRoot.JOB_CODE,
orgTreeOrder: orgRoot.orgRootOrder,
orgTreePhoneEx: orgRoot.orgRootPhoneEx,
orgTreePhoneIn: orgRoot.orgRootPhoneIn,
orgTreeFax: orgRoot.orgRootFax,
orgRevisionId: orgRoot.orgRevisionId,
orgRootName: orgRoot.orgRootName,
isDeputy: orgRoot.isDeputy,
isCommission: orgRoot.isCommission,
responsibility: orgRoot.responsibility,
labelName:
orgRoot.orgRootName + " " + orgRoot.orgRootCode + "00" + " " + orgRoot.orgRootShortName,
// totalPosition: rootCounts.totalPosition,
// totalPositionCurrentUse: rootCounts.totalPositionCurrentUse,
// totalPositionCurrentVacant: rootCounts.totalPositionCurrentVacant,
// totalPositionNextUse: rootCounts.totalPositionNextUse,
// totalPositionNextVacant: rootCounts.totalPositionNextVacant,
// totalRootPosition: rootPosCounts.totalRootPosition,
// totalRootPositionCurrentUse: rootPosCounts.totalRootPositionCurrentUse,
// totalRootPositionCurrentVacant: rootPosCounts.totalRootPositionCurrentVacant,
// totalRootPositionNextUse: rootPosCounts.totalRootPositionNextUse,
// totalRootPositionNextVacant: rootPosCounts.totalRootPositionNextVacant,
children: orgChild1Data
.filter((orgChild1) => orgChild1.orgRootId === orgRoot.id)
.map((orgChild1) => {
// const child1Counts = getCounts(orgChild1Map, orgChild1.id);
// const child1PosKey = `${orgRoot.id}-${orgChild1.id}`;
// const child1PosCounts = getRootCounts(rootPosMap, child1PosKey);
return {
orgTreeId: orgChild1.id,
orgRootId: orgRoot.id,
orgLevel: 1,
orgName: `${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`,
orgTreeName: orgChild1.orgChild1Name,
orgTreeShortName: orgChild1.orgChild1ShortName,
orgTreeCode: orgChild1.orgChild1Code,
orgCode: orgRoot.orgRootCode + orgChild1.orgChild1Code,
orgTreeRank: orgChild1.orgChild1Rank,
orgTreeRankSub: orgChild1.orgChild1RankSub,
orgRootDnaId: orgRoot.ancestorDNA,
orgChild1DnaId: orgChild1.ancestorDNA,
DEPARTMENT_CODE: orgChild1.DEPARTMENT_CODE,
DIVISION_CODE: orgChild1.DIVISION_CODE,
SECTION_CODE: orgChild1.SECTION_CODE,
JOB_CODE: orgChild1.JOB_CODE,
orgTreeOrder: orgChild1.orgChild1Order,
orgRootCode: orgRoot.orgRootCode,
orgTreePhoneEx: orgChild1.orgChild1PhoneEx,
orgTreePhoneIn: orgChild1.orgChild1PhoneIn,
orgTreeFax: orgChild1.orgChild1Fax,
orgRevisionId: orgRoot.orgRevisionId,
orgRootName: orgRoot.orgRootName,
responsibility: orgChild1.responsibility,
isOfficer: orgChild1.isOfficer,
isInformation: orgChild1.isInformation,
labelName:
orgChild1.orgChild1Name +
" " +
orgRoot.orgRootCode +
orgChild1.orgChild1Code +
" " +
orgChild1.orgChild1ShortName,
// totalPosition: child1Counts.totalPosition,
// totalPositionCurrentUse: child1Counts.totalPositionCurrentUse,
// totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant,
// totalPositionNextUse: child1Counts.totalPositionNextUse,
// totalPositionNextVacant: child1Counts.totalPositionNextVacant,
// totalRootPosition: child1PosCounts.totalRootPosition,
// totalRootPositionCurrentUse: child1PosCounts.totalRootPositionCurrentUse,
// totalRootPositionCurrentVacant: child1PosCounts.totalRootPositionCurrentVacant,
// totalRootPositionNextUse: child1PosCounts.totalRootPositionNextUse,
// totalRootPositionNextVacant: child1PosCounts.totalRootPositionNextVacant,
children: orgChild2Data
.filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id)
.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,
orgRootId: orgChild1.id,
orgLevel: 2,
orgName: `${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`,
orgTreeName: orgChild2.orgChild2Name,
orgTreeShortName: orgChild2.orgChild2ShortName,
orgTreeCode: orgChild2.orgChild2Code,
orgCode: orgRoot.orgRootCode + orgChild2.orgChild2Code,
orgTreeRank: orgChild2.orgChild2Rank,
orgTreeRankSub: orgChild2.orgChild2RankSub,
orgRootDnaId: orgRoot.ancestorDNA,
orgChild1DnaId: orgChild1.ancestorDNA,
orgChild2DnaId: orgChild2.ancestorDNA,
DEPARTMENT_CODE: orgChild2.DEPARTMENT_CODE,
DIVISION_CODE: orgChild2.DIVISION_CODE,
SECTION_CODE: orgChild2.SECTION_CODE,
JOB_CODE: orgChild2.JOB_CODE,
orgTreeOrder: orgChild2.orgChild2Order,
orgRootCode: orgRoot.orgRootCode,
orgTreePhoneEx: orgChild2.orgChild2PhoneEx,
orgTreePhoneIn: orgChild2.orgChild2PhoneIn,
orgTreeFax: orgChild2.orgChild2Fax,
orgRevisionId: orgRoot.orgRevisionId,
orgRootName: orgRoot.orgRootName,
responsibility: orgChild2.responsibility,
labelName:
orgChild2.orgChild2Name +
" " +
orgRoot.orgRootCode +
orgChild2.orgChild2Code +
" " +
orgChild2.orgChild2ShortName,
// totalPosition: child2Counts.totalPosition,
// totalPositionCurrentUse: child2Counts.totalPositionCurrentUse,
// totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant,
// totalPositionNextUse: child2Counts.totalPositionNextUse,
// totalPositionNextVacant: child2Counts.totalPositionNextVacant,
// totalRootPosition: child2PosCounts.totalRootPosition,
// totalRootPositionCurrentUse: child2PosCounts.totalRootPositionCurrentUse,
// totalRootPositionCurrentVacant: child2PosCounts.totalRootPositionCurrentVacant,
// totalRootPositionNextUse: child2PosCounts.totalRootPositionNextUse,
// totalRootPositionNextVacant: child2PosCounts.totalRootPositionNextVacant,
children: orgChild3Data
.filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id)
.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,
orgRootId: orgChild2.id,
orgLevel: 3,
orgName: `${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`,
orgTreeName: orgChild3.orgChild3Name,
orgTreeShortName: orgChild3.orgChild3ShortName,
orgTreeCode: orgChild3.orgChild3Code,
orgCode: orgRoot.orgRootCode + orgChild3.orgChild3Code,
orgTreeRank: orgChild3.orgChild3Rank,
orgTreeRankSub: orgChild3.orgChild3RankSub,
orgRootDnaId: orgRoot.ancestorDNA,
orgChild1DnaId: orgChild1.ancestorDNA,
orgChild2DnaId: orgChild2.ancestorDNA,
orgChild3DnaId: orgChild3.ancestorDNA,
DEPARTMENT_CODE: orgChild3.DEPARTMENT_CODE,
DIVISION_CODE: orgChild3.DIVISION_CODE,
SECTION_CODE: orgChild3.SECTION_CODE,
JOB_CODE: orgChild3.JOB_CODE,
orgTreeOrder: orgChild3.orgChild3Order,
orgRootCode: orgRoot.orgRootCode,
orgTreePhoneEx: orgChild3.orgChild3PhoneEx,
orgTreePhoneIn: orgChild3.orgChild3PhoneIn,
orgTreeFax: orgChild3.orgChild3Fax,
orgRevisionId: orgRoot.orgRevisionId,
orgRootName: orgRoot.orgRootName,
responsibility: orgChild3.responsibility,
labelName:
orgChild3.orgChild3Name +
" " +
orgRoot.orgRootCode +
orgChild3.orgChild3Code +
" " +
orgChild3.orgChild3ShortName,
// totalPosition: child3Counts.totalPosition,
// totalPositionCurrentUse: child3Counts.totalPositionCurrentUse,
// totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant,
// totalPositionNextUse: child3Counts.totalPositionNextUse,
// totalPositionNextVacant: child3Counts.totalPositionNextVacant,
// totalRootPosition: child3PosCounts.totalRootPosition,
// totalRootPositionCurrentUse: child3PosCounts.totalRootPositionCurrentUse,
// totalRootPositionCurrentVacant:
// child3PosCounts.totalRootPositionCurrentVacant,
// totalRootPositionNextUse: child3PosCounts.totalRootPositionNextUse,
// totalRootPositionNextVacant: child3PosCounts.totalRootPositionNextVacant,
children: orgChild4Data
.filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id)
.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,
orgRootId: orgChild3.id,
orgLevel: 4,
orgName: `${orgChild4.orgChild4Name}/${orgChild3.orgChild3Name}/${orgChild2.orgChild2Name}/${orgChild1.orgChild1Name}/${orgRoot.orgRootName}`,
orgTreeName: orgChild4.orgChild4Name,
orgTreeShortName: orgChild4.orgChild4ShortName,
orgTreeCode: orgChild4.orgChild4Code,
orgCode: orgRoot.orgRootCode + orgChild4.orgChild4Code,
orgTreeRank: orgChild4.orgChild4Rank,
orgTreeRankSub: orgChild4.orgChild4RankSub,
orgRootDnaId: orgRoot.ancestorDNA,
orgChild1DnaId: orgChild1.ancestorDNA,
orgChild2DnaId: orgChild2.ancestorDNA,
orgChild3DnaId: orgChild3.ancestorDNA,
orgChild4DnaId: orgChild4.ancestorDNA,
DEPARTMENT_CODE: orgChild4.DEPARTMENT_CODE,
DIVISION_CODE: orgChild4.DIVISION_CODE,
SECTION_CODE: orgChild4.SECTION_CODE,
JOB_CODE: orgChild4.JOB_CODE,
orgTreeOrder: orgChild4.orgChild4Order,
orgRootCode: orgRoot.orgRootCode,
orgTreePhoneEx: orgChild4.orgChild4PhoneEx,
orgTreePhoneIn: orgChild4.orgChild4PhoneIn,
orgTreeFax: orgChild4.orgChild4Fax,
orgRevisionId: orgRoot.orgRevisionId,
orgRootName: orgRoot.orgRootName,
responsibility: orgChild4.responsibility,
labelName:
orgChild4.orgChild4Name +
" " +
orgRoot.orgRootCode +
orgChild4.orgChild4Code +
" " +
orgChild4.orgChild4ShortName,
// totalPosition: child4Counts.totalPosition,
// totalPositionCurrentUse: child4Counts.totalPositionCurrentUse,
// totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant,
// totalPositionNextUse: child4Counts.totalPositionNextUse,
// totalPositionNextVacant: child4Counts.totalPositionNextVacant,
// totalRootPosition: child4PosCounts.totalRootPosition,
// totalRootPositionCurrentUse:
// child4PosCounts.totalRootPositionCurrentUse,
// totalRootPositionCurrentVacant:
// child4PosCounts.totalRootPositionCurrentVacant,
// totalRootPositionNextUse: child4PosCounts.totalRootPositionNextUse,
// totalRootPositionNextVacant:
// child4PosCounts.totalRootPositionNextVacant,
children: [],
};
}),
};
}),
};
}),
};
}),
};
});
return new HttpSuccess(formattedData);
}

View file

@ -17,10 +17,8 @@ class LogMemoryStore {
rootIdCache: new Map(),
updatedAt: new Date(),
};
private readonly REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
private isRefreshing = false;
private readonly CACHE_TTL = 60 * 60 * 1000; // 60 minutes
private isInitialized = false;
private refreshTimer: NodeJS.Timeout | null = null;
constructor() {
// ไม่ refresh ทันที - รอให้เรียก initialize() หลัง TypeORM ready
@ -35,24 +33,15 @@ class LogMemoryStore {
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",
this.CACHE_TTL / 1000 / 60,
"minute TTL (passive cleanup on access)",
);
}
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);
@ -72,12 +61,26 @@ class LogMemoryStore {
console.log("[LogMemoryStore] Cache refreshed at", this.cache.updatedAt.toISOString());
} catch (error) {
console.error("[LogMemoryStore] Error refreshing cache:", error);
} finally {
this.isRefreshing = false;
}
}
// Check if cache is stale and refresh if needed
private async checkAndRefreshIfNeeded() {
const now = new Date();
const age = now.getTime() - this.cache.updatedAt.getTime();
if (age > this.CACHE_TTL) {
console.log(
"[LogMemoryStore] Cache is stale (age:",
Math.round(age / 1000 / 60),
"minutes), refreshing...",
);
await this.refreshCache();
}
}
getCurrentRevision(): OrgRevision | null {
// Check for stale data (fire and forget)
this.checkAndRefreshIfNeeded();
return this.cache.currentRevision;
}
@ -89,6 +92,9 @@ class LogMemoryStore {
* Get Profile by keycloak ID with caching
*/
async getProfileByKeycloak(keycloak: string): Promise<Profile | null> {
// Check for stale data
await this.checkAndRefreshIfNeeded();
// Check cache first
if (this.cache.profileCache.has(keycloak)) {
return this.cache.profileCache.get(keycloak)!;
@ -114,6 +120,9 @@ class LogMemoryStore {
async getRootIdByProfileId(profileId: string | undefined): Promise<string | null> {
if (!profileId) return null;
// Check for stale data
await this.checkAndRefreshIfNeeded();
// Check cache first
if (this.cache.rootIdCache.has(profileId)) {
return this.cache.rootIdCache.get(profileId)!;
@ -148,10 +157,8 @@ class LogMemoryStore {
// สำหรับ shutdown
destroy() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
// No active timer to clear
console.log("[LogMemoryStore] Destroyed");
}
}

View file

@ -5,7 +5,7 @@ interface CacheEntry {
class OrgStructureCache {
private cache: Map<string, CacheEntry> = new Map();
private readonly CACHE_TTL = 10 * 60 * 1000; // 10 minutes
private readonly CACHE_TTL = 30 * 60 * 1000; // 30 minutes
private isInitialized = false;
private cleanupTimer: NodeJS.Timeout | null = null;
@ -17,7 +17,7 @@ class OrgStructureCache {
if (this.isInitialized) return;
this.isInitialized = true;
// Cleanup expired entries every 10 minutes
// Cleanup expired entries every 30 minutes
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.CACHE_TTL);