Merge branch 'fix/optimization-detailSuperAdmin' into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
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:
commit
34f4a01d31
6 changed files with 694 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
287
docs/SUMMARY_OPTIMIZATION-fix-optimization.md
Normal file
287
docs/SUMMARY_OPTIMIZATION-fix-optimization.md
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
368
src/controllers/super-admin-{id}-bak.txt
Normal file
368
src/controllers/super-admin-{id}-bak.txt
Normal 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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue