Compare commits
1 commit
dev
...
cleanupUp_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8201ac51af |
14 changed files with 855 additions and 143 deletions
|
|
@ -11,6 +11,9 @@
|
|||
"build": "tsoa spec-and-routes && tsc",
|
||||
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts",
|
||||
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts",
|
||||
"seed:show": "typeorm-ts-node-commonjs migration:show -d src/seed/data-source.ts",
|
||||
"seed:run": "typeorm-ts-node-commonjs migration:run -d src/seed/data-source.ts",
|
||||
"seed:revert": "typeorm-ts-node-commonjs migration:revert -d src/seed/data-source.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
|
|
|
|||
263
plans/plan_cleanup-mock-seed.md
Normal file
263
plans/plan_cleanup-mock-seed.md
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
# Plan + สรุปตาราง + Drop Script — Cleanup & Mock Seed `hrms_organization`
|
||||
|
||||
> **ไฟล์เดียวจบ:** Part A สรุปตาราง DB · Part B plan checklist (CP0-CP7) · Part C drop script SQL
|
||||
> **Status:** ✅ CP0-CP6 done (drop 54 tables + seed mock org/pos/profile) · ⏳ CP7 (Keycloak) defer
|
||||
> **Seed mock:** แยกที่ `src/seed/` → `npm run seed:run` (ไม่กระทบ `migration:run` หลัก)
|
||||
> **วิธีใช้:** เช็ค `- [x]` ทีละขั้น, ทุก CP destructive ต้องแจ้ง user ก่อน + รอยืนยัน (DB test `192.168.1.68` เท่านั้น)
|
||||
|
||||
---
|
||||
|
||||
# Part A — สรุปตาราง DB
|
||||
|
||||
| รายการ | ค่า |
|
||||
|---|---|
|
||||
| Database | `hrms_organization` (MySQL 8.4.6) |
|
||||
| Host | test `192.168.1.68` / prod `192.168.1.63` (ห้ามแตะ prod) |
|
||||
| รวมตาราง | **211 → 157** (drop 54 แล้ว @ test) |
|
||||
|
||||
สถานะการใช้งาน:
|
||||
|
||||
| สถานะ | จำนวน | รายละเอียด |
|
||||
|---|---:|---|
|
||||
| ✅ ใช้งานจริง (เก็บไว้) | **147** | ข้อมูลธุรกิจหลัก + Master Data + ตารางเสริมที่มี controller |
|
||||
| 🗑️ เตรียมลบ/ลบแล้ว | **54** | ดู §5 |
|
||||
| ⚙️ ระบบ/ตัวเชื่อม | **10** | junction + system tables |
|
||||
| **รวม** | **211** | |
|
||||
|
||||
## A.1 ข้อมูลหลัก
|
||||
| กลุ่ม | ตัวอย่างตาราง |
|
||||
|---|---|
|
||||
| โครงสร้างองค์กร | `orgRoot`, `orgChild1`–`orgChild4`, `orgRevision` |
|
||||
| ประวัติพนักงาน | `profile`, `profileEmployee`, `profileSalary`, `profile*` + `*History` |
|
||||
| ตำแหน่ง/อัตรากำลัง | `position`, `posMaster`, `posMasterHistory`, `employeePosition` |
|
||||
| คำสั่ง | `command`, `commandSend`, `commandRecive`, `commandOperator` |
|
||||
| สิทธิ์/การเข้าถึง | `authRole`, `permissionOrg`, `permissionProfile`, `roleKeycloak` |
|
||||
| อายุงาน/วิทยฐานะ | `tenure*` (5 ตาราง) |
|
||||
|
||||
## A.2 Master Data (ข้อมูลอ้างอิง — lookup, เปลี่ยนน้อย)
|
||||
| กลุ่ม | ตาราง (จำนวนแถว exact) |
|
||||
|---|---|
|
||||
| ที่อยู่ | `province` (77), `district` (1,051), `subDistrict` (8,585), `province_master`, `district_master`, `subdistrict_master` |
|
||||
| ประวัติบุคคล | `gender` (3), `bloodGroup` (4), `religion` (5), `relationship` (4), `prefixe` (5), `rank` (9), `educationLevel` (14) |
|
||||
| ตำแหน่ง | `posType` (4), `posLevel` (15), `posDict` (910), `posExecutive` (40), `employeePosType`, `employeePosLevel`, `employeePosDict` |
|
||||
| เครื่องราช | `insignia` (14), `insigniaType` (3) |
|
||||
| คำสั่ง/การลา | `commandType` (47), `commandCode` (21), `commandSys` (12), `leaveType` (11) |
|
||||
|
||||
> 💡 ที่อยู่มี 2 ชุดซ้ำซ้อน: `province`/`district`/`subDistrict` (ใช้จริง) vs `*_master` (ชุดสำรอง) — ตรวจสอบว่าจำเป็นไหม
|
||||
|
||||
## A.3 ตารางคำนวณอัตโนมัติ (cron เขียนทุกคืน — ห้ามแก้ด้วยมือ)
|
||||
| เวลา | ตาราง | สิ่งที่ทำ |
|
||||
|---|---|---|
|
||||
| 00:30 ทุกคืน | `profile`, `profileEmployee` | ประมวลผลคำสั่ง |
|
||||
| 04:00 ทุกคืน | `registry`, `registryEmployee`, `tenure*` | **ล้างและสร้างใหม่** (full refresh) |
|
||||
| 02:00 วันที่ 1 ต.ค. | `profile`, `profileEmployee`, `profileSalary`, `posMaster` | อัปเดตสถานะเกษียณ |
|
||||
|
||||
## A.4 ข้อมูลต้นทางจากระบบเดิม (MIS)
|
||||
ชื่อตารางตัวใหญ่ (`HR_*`, `EMPLOYEE`, `Profile*`):
|
||||
| สถานะ | จำนวน | รายละเอียด |
|
||||
|---|---:|---|
|
||||
| ใช้อยู่ (สำหรับนำเข้าข้อมูล) | 22 | `EMPLOYEE`, `OFFICER`, `Profile*` (ตัวใหญ่), `*Import` |
|
||||
| ไม่ได้ใช้แล้ว (dead entity) | 25 | `HR_*` (24) + `IMPORT_ORG` — เคยมีข้อมูลจริง >1.4M แถว |
|
||||
|
||||
> ⚠️ ทั้ง 47 ตารางอยู่ในกลุ่มเตรียมลบ (§5) — **drop แล้ว @ test**
|
||||
|
||||
## A.5 กลุ่มที่เตรียมลบ/ลบแล้ว (54 ตาราง)
|
||||
| กลุ่ม | จำนวน | ตัวอย่าง / เหตุผล |
|
||||
|---|---:|---|
|
||||
| ข้อมูลระบบเดิม (MIS) | 47 | `HR_*`, `EMPLOYEE`, `OFFICER`, `Profile*`, `*Import`, `IMPORT_ORG` |
|
||||
| ตารางพักนำเข้า (ว่าง) | 2 | `HR_CHILDEN_EMPTEMP`, `HR_INSIGNIA_EMPTEMP` (0 แถว) |
|
||||
| ตารางซ้ำ/ทดสอบ/เก่า | 5 | `Profiles`, `new_table`, `new_table_emp`, `entity_base`, `MET_PREFIX` |
|
||||
|
||||
หลังลบครบเหลือ **157 ตาราง**. Script อยู่ Part C.
|
||||
|
||||
> ⚠️ `profile*Employee*` (profileDisciplineEmployee, profileInformation, profileSalaryEmployee + *History) **ยังใช้** (มี controller: ProfileDisciplineEmployeeController, ProfileSalaryEmployeeController, ProfileEmployeeController) — อย่า drop
|
||||
|
||||
## A.6 ข้อควรระวัง
|
||||
- ตารางเก็บ **ชื่อ + เลขบัตรประชาชน**: ~23 ตาราง (หลัก `profile`, `profileEmployee`, `registry`) — ข้อมูลส่วนบุคคล
|
||||
- `migrations`, `typeorm_metadata` เป็นตารางระบบ **ห้ามลบเด็ดขาด**
|
||||
- จำนวนแถวใน §A.1/A.4 เป็นค่าประมาณ (ยกเว้น §A.2 Master Data = exact)
|
||||
|
||||
## A.7 Seed / Mock data (สำหรับ dev/test)
|
||||
mock data แยกที่ `src/seed/` (Node/TS) → `npm run seed:show` / `seed:run` / `seed:revert`
|
||||
seed **แยกจาก migration หลัก** → `migration:run` ไม่เจอ seed → ไม่ทำลายข้อมูล prod. อ่าน `src/seed/README.md`
|
||||
|
||||
---
|
||||
|
||||
# Part B — Plan checklist (CP0-CP7)
|
||||
|
||||
## Context
|
||||
DB `hrms_organization` (test) มีข้อมูลจริง + ตารางระบบเดิมไม่ได้ใช้ → ทำ environment สะอาด: ลดตารางเหลือใช้จริง + แทนข้อมูลจริงด้วย mock 30 คน + โครงสร้าง mock (revision 2569) + (defer) Keycloak
|
||||
|
||||
## Decisions
|
||||
| | |
|
||||
|---|---|
|
||||
| Target DB | `192.168.1.68` (test) — **แจ้ง user ทุกครั้งก่อนจัดการ data** |
|
||||
| Mock | 30 คน = 20 ข้าราชการ (Profile) + 10 ลูกจ้าง (ProfileEmployee) |
|
||||
| citizenId | สุ่ม 13 หลักผ่าน checksum จริง + ห้ามตรงของเดิม (block list) |
|
||||
| HR_* (24) | drop เลย (ไม่รอ .NET) |
|
||||
| Keycloak | clear `keycloak` column ก่อนลบ profile → CP6 clear orphan → CP7 ผูก KC ใหม่ |
|
||||
| CP7 password | `ddMMyyyy` พ.ศ. จาก birthDate mock |
|
||||
|
||||
## Checklist
|
||||
|
||||
### CP0 — Pre-flight ✅
|
||||
- [x] หยุด service (กัน cron 00:30/03:00/04:00 ทับ) + ยืนยันต่อ DB `192.168.1.68` (clone: 211 tables, profile 20,501)
|
||||
- [x] verify junction/child table names + NOT NULL columns ของ target tables
|
||||
|
||||
### เตรียมไฟล์ ✅
|
||||
- [x] `src/seed/mockData.ts` (ชื่อไทย + genValidCitizenId checksum + deterministic UUID + genBirthDate)
|
||||
- [x] 5 seed migrations (CP3-CP5c) + birthDate ใน CP5b; typecheck + test ผ่าน
|
||||
|
||||
### CP1 — Full Backup ✅ SKIP (user มี backup อยู่แล้ว)
|
||||
|
||||
### CP2 — Drop 54 unused tables (211 → 157) ✅ done
|
||||
- [x] comment `ImportDataController.ts` @Route/@Security → `npm run build` → verify `/api/v1/org/upload/*` หาย
|
||||
- [x] execute DROP 54 tables ใต้ `SET FOREIGN_KEY_CHECKS=0/1` (ดู Part C)
|
||||
- [x] **verify**: tables = **157**; `migrations`/`typeorm_metadata` ยังอยู่
|
||||
|
||||
### CP3 — Seed orgRevision 2569 ✅ done
|
||||
- [x] run `1782100000001-seed_org_revision_2569.ts` (ล้าง org/pos เดิม + INSERT "โครงสร้าง 2569")
|
||||
- [x] **verify**: `orgRevision`=1, `orgRevisionIsCurrent`=1
|
||||
|
||||
### CP4 — Seed mock org structure ✅ done
|
||||
- [x] run `1782100000002-seed_mock_org_structure.ts`
|
||||
- [x] **verify**: สำนักA/หน่วยงานA/ฝ่ายA/กลุ่มงานA; FK chain ครบ
|
||||
|
||||
### CP5a — Seed mock positions ✅ done (seed ตรง — TypeORM INSERT quirk)
|
||||
- [x] run `1782100000003-seed_mock_positions.ts` (20 posMaster + 10 employeePosMaster)
|
||||
- [x] **verify**: posMaster=20, employeePosMaster=10; current_holderId NULL
|
||||
|
||||
### CP5b — Seed mock profiles + clear keycloak ✅ done
|
||||
- [x] run `1782100000004-seed_mock_profiles.ts` (snapshot citizenId block list, clear keycloak, DELETE child + profile, INSERT 30 mock + birthDate)
|
||||
- [x] **verify**: profile=20, profileEmployee=10; citizenId checksum + ไม่ตรง blocklist; birthDate ครบ
|
||||
|
||||
### CP5c — Link holders ✅ done
|
||||
- [x] run `1782100000005-seed_link_holders.ts`
|
||||
- [x] **verify**: posMaster holder=20 join profile ครบ; employeePosMaster holder=10
|
||||
|
||||
### CP6 — Clear Keycloak orphans ✅ SKIP (จัด KC ท้ายสุดพร้อม CP7)
|
||||
|
||||
### CP7 — ผูก Keycloak ให้ mock ⏳ defer
|
||||
- [ ] เปลี่ยน `.env` `KC_SERVICE_ACCOUNT_*` ชี้ Keycloak **test**
|
||||
- [ ] seed migration (`1782100000006-seed_keycloak_users.ts`) เรียก helper `src/keycloak/index.ts` (createUser/addUserRoles/getRoles/getUserByUsername)
|
||||
- [ ] role `WHERE name='USER'` (แทน hardcode UUID `8a1a0dc9-...`)
|
||||
- [ ] loop mock 30 คน: createUser(citizenId, password `ddMMyyyy` พ.ศ. จาก birthDate) + addUserRoles(USER) + เซ็ต `profile.keycloak` + junction
|
||||
- [ ] **verify**: KC user = mock count; login mock ได้; super_admin ยังอยู่
|
||||
|
||||
## ไฟล์ที่สร้างแล้ว
|
||||
**Seed (แยกจาก migration หลัก):** `src/seed/README.md`, `src/seed/data-source.ts`, `src/seed/mockData.ts`, `src/seed/migrations/178210000000{1..5}-seed_*.ts`
|
||||
**npm scripts:** `npm run seed:show` / `seed:run` / `seed:revert` (ใช้ `src/seed/data-source.ts`)
|
||||
**Code:** `src/controllers/ImportDataController.ts` (@Route/@Security comment — CP2)
|
||||
|
||||
## End-to-end verify (หลัง CP6)
|
||||
1. `npm run migration:run` ผ่านไม่ error (migration หลัก) + `npm run seed:show` เห็น seed 5
|
||||
2. tables = 157
|
||||
3. profile=20, profileEmployee=10, orgRevision=1
|
||||
4. citizenId ผ่าน checksum + ไม่ตรง blocklist
|
||||
5. posMaster holder=20 join profile ครบ
|
||||
6. cron 04:00 รอบแรก → registry/tenure rebuild จาก mock 30 คน
|
||||
7. login admin ได้ (super_admin ยังอยู่)
|
||||
|
||||
## Risks
|
||||
- migration + `SET FOREIGN_KEY_CHECKS` session-level ไม่ rollback → **backup เป็นหลัก ไม่พึ่ง down()**
|
||||
- cron ทับ → หยุด service หรือทำ CP3-5c ในกรอบเวลาแคบ
|
||||
- CP7 env ต้องชี้ KC test ก่อน
|
||||
- TypeORM quirk: CP5a INSERT SELECT WHERE NOT EXISTS อาจไม่ execute ใน transaction → seed ตรงถ้า posMaster ว่าง
|
||||
|
||||
---
|
||||
|
||||
# Part C — Drop script SQL (CP2)
|
||||
|
||||
> ⚠️⚠️⚠️ DROP TABLE ลบถาวร ย้อนกลับไม่ได้. ทุก DROP comment (`--`) ไว้ — uncomment เฉพาะที่ตรวจสอบแล้ว.
|
||||
> ก่อนรัน: (1) backup DB (2) comment `ImportDataController` + `npm run build` (3) รันบน test เท่านั้น (4) ปรึกษาทีม .NET เรื่อง HR_*
|
||||
> 🚫 ห้าม DROP: `migrations`, `typeorm_metadata`
|
||||
|
||||
```sql
|
||||
USE hrms_organization;
|
||||
|
||||
-- นับตารางก่อนรัน (ควรได้ 211)
|
||||
SELECT COUNT(*) AS tables_before
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'hrms_organization' AND table_type = 'BASE TABLE';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================================
|
||||
-- กลุ่ม 1 — MIS tables (47) — ⚠️ ImportDataController พังหลัง drop → comment controller ก่อน
|
||||
-- ============================================================
|
||||
-- -- บุคลากรต้นทาง (4)
|
||||
-- DROP TABLE IF EXISTS EMPLOYEE;
|
||||
-- DROP TABLE IF EXISTS EMPLOYEETEMP;
|
||||
-- DROP TABLE IF EXISTS OFFICER;
|
||||
-- DROP TABLE IF EXISTS positionOfficer;
|
||||
-- -- Profile* staging ตัว P ใหญ่ (14)
|
||||
-- DROP TABLE IF EXISTS ProfileAbilitys;
|
||||
-- DROP TABLE IF EXISTS ProfileCertificates;
|
||||
-- DROP TABLE IF EXISTS ProfileChangeNames;
|
||||
-- DROP TABLE IF EXISTS ProfileChildrens;
|
||||
-- DROP TABLE IF EXISTS ProfileDisciplines;
|
||||
-- DROP TABLE IF EXISTS ProfileDutys;
|
||||
-- DROP TABLE IF EXISTS ProfileEducations;
|
||||
-- DROP TABLE IF EXISTS ProfileHonors;
|
||||
-- DROP TABLE IF EXISTS ProfileInsignias;
|
||||
-- DROP TABLE IF EXISTS ProfileLeaveSummary;
|
||||
-- DROP TABLE IF EXISTS ProfileNopaids;
|
||||
-- DROP TABLE IF EXISTS ProfileOthers;
|
||||
-- DROP TABLE IF EXISTS ProfileSalaries;
|
||||
-- DROP TABLE IF EXISTS ProfileTrainings;
|
||||
-- -- Import master (4)
|
||||
-- DROP TABLE IF EXISTS amphurImport;
|
||||
-- DROP TABLE IF EXISTS educationMis;
|
||||
-- DROP TABLE IF EXISTS provinceImport;
|
||||
-- DROP TABLE IF EXISTS subDistrictImport;
|
||||
-- -- HR_* dead entity (24) — ⛔ มีข้อมูลจริง >1.4M แถว ปรึกษาทีม .NET ก่อน!
|
||||
-- DROP TABLE IF EXISTS HR_CHANGENAME;
|
||||
-- DROP TABLE IF EXISTS HR_CHANGENAME_EMP;
|
||||
-- DROP TABLE IF EXISTS HR_CHANGENAME_EMPTEMP;
|
||||
-- DROP TABLE IF EXISTS HR_CHILDEN;
|
||||
-- DROP TABLE IF EXISTS HR_CHILDEN_EMP;
|
||||
-- DROP TABLE IF EXISTS HR_DISCIPLINE;
|
||||
-- DROP TABLE IF EXISTS HR_DISCIPLINE_EMP;
|
||||
-- DROP TABLE IF EXISTS HR_DISCIPLINE_EMPTEMP;
|
||||
-- DROP TABLE IF EXISTS HR_EDUCATION;
|
||||
-- DROP TABLE IF EXISTS HR_EDUCATION_EMP;
|
||||
-- DROP TABLE IF EXISTS HR_EDUCATION_EMPTEMP;
|
||||
-- DROP TABLE IF EXISTS HR_FUND_COURSE_CODE;
|
||||
-- DROP TABLE IF EXISTS HR_INSIGNIA;
|
||||
-- DROP TABLE IF EXISTS HR_INSIGNIA_EMP;
|
||||
-- DROP TABLE IF EXISTS HR_MAJOR_CODE;
|
||||
-- DROP TABLE IF EXISTS HR_PERSONAL_EMPTEMP_ADDRESS;
|
||||
-- DROP TABLE IF EXISTS HR_PERSONAL_EMPTEMP_FAMILY;
|
||||
-- DROP TABLE IF EXISTS HR_PERSONAL_EMP_ADDRESS;
|
||||
-- DROP TABLE IF EXISTS HR_PERSONAL_EMP_FAMILY;
|
||||
-- DROP TABLE IF EXISTS HR_PERSONAL_OFFICER_ADDRESS;
|
||||
-- DROP TABLE IF EXISTS HR_PERSONAL_OFFICER_FAMILY;
|
||||
-- DROP TABLE IF EXISTS HR_POSITION_EMPLOYEE;
|
||||
-- DROP TABLE IF EXISTS HR_POSITION_EMPLOYEETEMP;
|
||||
-- DROP TABLE IF EXISTS HR_POSITION_OFFICER;
|
||||
-- -- IMPORT_ORG (0 แถว)
|
||||
-- DROP TABLE IF EXISTS IMPORT_ORG;
|
||||
|
||||
-- ============================================================
|
||||
-- กลุ่ม 2 — Import staging ไม่มี entity (2, 0 แถว)
|
||||
-- ============================================================
|
||||
-- DROP TABLE IF EXISTS HR_CHILDEN_EMPTEMP;
|
||||
-- DROP TABLE IF EXISTS HR_INSIGNIA_EMPTEMP;
|
||||
|
||||
-- ============================================================
|
||||
-- กลุ่ม 3 — ซ้ำ/ทดสอบ/เก่า (5)
|
||||
-- ⚠️ profile*Employee* ยังใช้ (มี controller) — อย่า drop
|
||||
-- ============================================================
|
||||
-- DROP TABLE IF EXISTS Profiles;
|
||||
-- DROP TABLE IF EXISTS new_table;
|
||||
-- DROP TABLE IF EXISTS new_table_emp;
|
||||
-- DROP TABLE IF EXISTS entity_base;
|
||||
-- DROP TABLE IF EXISTS MET_PREFIX;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- นับตารางหลังรัน (drop ครบ 54 → เหลือ 157)
|
||||
SELECT COUNT(*) AS tables_after
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'hrms_organization' AND table_type = 'BASE TABLE';
|
||||
```
|
||||
|
|
@ -43,35 +43,6 @@ class TokenCache {
|
|||
}
|
||||
}
|
||||
|
||||
// network/DNS error codes ที่ถือว่าเป็น transient (ควร retry)
|
||||
const TRANSIENT_NETWORK_CODES = [
|
||||
"EAI_AGAIN", // DNS lookup ล้มเหลวชั่วคราว
|
||||
"ENOTFOUND",
|
||||
"ECONNRESET",
|
||||
"ETIMEDOUT",
|
||||
"ECONNREFUSED",
|
||||
"EHOSTUNREACH",
|
||||
"ENETUNREACH",
|
||||
];
|
||||
|
||||
// ตรวจว่า error ควร retry: ครอบ network/DNS (ไม่มี response หรือ code ตรง TRANSIENT) และ HTTP 5xx
|
||||
function isTransientError(error: any): boolean {
|
||||
if (!error) return false;
|
||||
if (!error.response || TRANSIENT_NETWORK_CODES.includes(error.code)) {
|
||||
return true;
|
||||
}
|
||||
return error.response?.status >= 500;
|
||||
}
|
||||
|
||||
// หน่วงเวลา (backoff) ระหว่าง retry — ใช้รูปแบบ setTimeout เดียวกับ keycloak/index.ts
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// config retry สำหรับเรียก Exprofile
|
||||
const EXPROFILE_MAX_RETRIES = 3; // จากเดิม 2
|
||||
const EXPROFILE_BACKOFF_BASE_MS = 1000; // backoff = base * 2^retryCount → 1s, 2s
|
||||
|
||||
@Route("api/v1/org/ex/retirement")
|
||||
@Tags("ExRetirement")
|
||||
@Security("bearerAuth")
|
||||
|
|
@ -89,7 +60,7 @@ export class ExRetirementController extends Controller {
|
|||
},
|
||||
) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = EXPROFILE_MAX_RETRIES;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
|
|
@ -121,9 +92,8 @@ export class ExRetirementController extends Controller {
|
|||
// return res.data;
|
||||
return new HttpSuccess(res.data.data);
|
||||
} catch (error: any) {
|
||||
if (isTransientError(error) && retryCount < maxRetries - 1) {
|
||||
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
|
||||
TokenCache.delete(`${clientId}:${clientSecret}`);
|
||||
await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount);
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -137,7 +107,7 @@ export class ExRetirementController extends Controller {
|
|||
@Get("/document/{documentId}")
|
||||
async getDocument(@Path("documentId") officerDocumentID: string, @Request() req: any) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = EXPROFILE_MAX_RETRIES;
|
||||
const maxRetries = 2;
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
const token = await getToken(clientId, clientSecret);
|
||||
|
|
@ -166,9 +136,8 @@ export class ExRetirementController extends Controller {
|
|||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (isTransientError(error) && retryCount < maxRetries - 1) {
|
||||
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
|
||||
TokenCache.delete(`${clientId}:${clientSecret}`);
|
||||
await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount);
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -202,13 +171,8 @@ async function getToken(ClientID: string, ClientSecret: string): Promise<string>
|
|||
TokenCache.set(cacheKey, token);
|
||||
return token;
|
||||
} catch (error) {
|
||||
// log แบบกระชับ (ลด log noise จากการ dump object ใหญ่ตามที่เห็นใน log จริง)
|
||||
console.error(
|
||||
"getToken error:",
|
||||
error instanceof Error ? `${error.name}: ${error.message}` : error,
|
||||
);
|
||||
// โยน AxiosError ตัวจริงออกไปให้ caller อ่าน code / response / status เพื่อตัดสินใจ retry
|
||||
throw error;
|
||||
console.error('getToken error:', error);
|
||||
return Promise.reject({ message: "Error occurred", error });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +198,7 @@ export async function PostRetireToExprofile(
|
|||
}
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = EXPROFILE_MAX_RETRIES;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
|
|
@ -271,9 +235,8 @@ export async function PostRetireToExprofile(
|
|||
|
||||
return res.data;
|
||||
} catch (error: any) {
|
||||
if (isTransientError(error) && retryCount < maxRetries - 1) {
|
||||
if (error.response?.status === 500 && retryCount < maxRetries - 1) {
|
||||
TokenCache.delete(`${clientId}:${clientSecret}`);
|
||||
await sleep(EXPROFILE_BACKOFF_BASE_MS * 2 ** retryCount);
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,9 +106,13 @@ import { ProvinceMaster } from "../entities/ProvinceMaster";
|
|||
import { SubDistrictMaster } from "../entities/SubDistrictMaster";
|
||||
import { DistrictMaster } from "../entities/DistrictMaster";
|
||||
import { RequestWithUser } from "../middlewares/user";
|
||||
@Route("api/v1/org/upload")
|
||||
@Tags("UPLOAD")
|
||||
@Security("bearerAuth")
|
||||
// ⛔ DISABLED 2026-07-21 — MIS tables ทั้งหมดจะถูก DROP (ดู .claude/docs/2026-07-21-drop-unused-tables.sql)
|
||||
// controller นี้เรียกใช้ mis entities (Profile*, EMPLOYEE, HR_*, ...) ผ่าน getRepository แทบทุก endpoint
|
||||
// ถ้าปล่อยไว้ → API /api/v1/org/upload/* จะ error ตอน runtime หลัง drop tables
|
||||
// วิธีคืนค้าง: uncomment ทั้ง 3 บรรทัดล่างนี้ + รัน `npm run build` (หลัง restore tables)
|
||||
// @Route("api/v1/org/upload")
|
||||
// @Tags("UPLOAD")
|
||||
// @Security("bearerAuth")
|
||||
export class ImportDataController extends Controller {
|
||||
private profileEducationRepo = AppDataSource.getRepository(ProfileEducation);
|
||||
private profileDisciplineRepo = AppDataSource.getRepository(ProfileDiscipline);
|
||||
|
|
|
|||
|
|
@ -11451,10 +11451,11 @@ export class ProfileController extends Controller {
|
|||
system?: string;
|
||||
},
|
||||
) {
|
||||
// ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ
|
||||
let _system: string = "SYS_REGISTRY_OFFICER";
|
||||
if (body.system) _system = body.system;
|
||||
let _data = await new permission().PermissionOrgList(request, _system);
|
||||
// comment ออกก่อนเพราะยังไม่ได้ใช้
|
||||
// // ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ
|
||||
// let _system: string = "SYS_REGISTRY_OFFICER";
|
||||
// if (body.system) _system = body.system;
|
||||
// let _data = await new permission().PermissionOrgList(request, _system);
|
||||
const findRevision = await this.orgRevisionRepo.findOne({
|
||||
where: { orgRevisionIsCurrent: true },
|
||||
});
|
||||
|
|
@ -11499,50 +11500,10 @@ export class ProfileController extends Controller {
|
|||
.leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
|
||||
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
|
||||
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
|
||||
.where("profile.isActive = :isActive AND profile.isDelete = :isDelete", {
|
||||
isActive: true,
|
||||
isDelete: false,
|
||||
.where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", {
|
||||
isActive: false,
|
||||
isDelete: true,
|
||||
})
|
||||
.andWhere(
|
||||
_data.root != undefined && _data.root != null
|
||||
? _data.root[0] != null
|
||||
? `current_holders.orgRootId IN (:...root)`
|
||||
: `current_holders.orgRootId is null`
|
||||
: "1=1",
|
||||
{ root: _data.root },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child1 != undefined && _data.child1 != null
|
||||
? _data.child1[0] != null
|
||||
? `current_holders.orgChild1Id IN (:...child1)`
|
||||
: `current_holders.orgChild1Id is null`
|
||||
: "1=1",
|
||||
{ child1: _data.child1 },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child2 != undefined && _data.child2 != null
|
||||
? _data.child2[0] != null
|
||||
? `current_holders.orgChild2Id IN (:...child2)`
|
||||
: `current_holders.orgChild2Id is null`
|
||||
: "1=1",
|
||||
{ child2: _data.child2 },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child3 != undefined && _data.child3 != null
|
||||
? _data.child3[0] != null
|
||||
? `current_holders.orgChild3Id IN (:...child3)`
|
||||
: `current_holders.orgChild3Id is null`
|
||||
: "1=1",
|
||||
{ child3: _data.child3 },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child4 != undefined && _data.child4 != null
|
||||
? _data.child4[0] != null
|
||||
? `current_holders.orgChild4Id IN (:...child4)`
|
||||
: `current_holders.orgChild4Id is null`
|
||||
: "1=1",
|
||||
{ child4: _data.child4 },
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` });
|
||||
|
|
|
|||
|
|
@ -6172,7 +6172,6 @@ export class ProfileEmployeeController extends Controller {
|
|||
*/
|
||||
@Post("search-personal-no-keycloak")
|
||||
async getProfileBySearchKeywordNoKeyCloak(
|
||||
@Request() request: RequestWithUser,
|
||||
@Query("page") page: number = 1,
|
||||
@Query("pageSize") pageSize: number = 10,
|
||||
@Body()
|
||||
|
|
@ -6182,10 +6181,6 @@ export class ProfileEmployeeController extends Controller {
|
|||
system?: string;
|
||||
},
|
||||
) {
|
||||
// ค้นหารายชื่อถ้าไม่ส่ง system มาให้ default ตามทะเบียนประวัติ
|
||||
let _system: string = "SYS_REGISTRY_EMP";
|
||||
if (body.system) _system = body.system;
|
||||
let _data = await new permission().PermissionOrgList(request, _system);
|
||||
const findRevision = await this.orgRevisionRepo.findOne({
|
||||
where: { orgRevisionIsCurrent: true },
|
||||
});
|
||||
|
|
@ -6230,50 +6225,10 @@ export class ProfileEmployeeController extends Controller {
|
|||
.leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
|
||||
.leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
|
||||
.leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
|
||||
.where("profile.isActive = :isActive AND profile.isDelete = :isDelete", {
|
||||
isActive: true,
|
||||
isDelete: false,
|
||||
.where(body.system ? "profile.isActive = :isActive" : "profile.isDelete = :isDelete", {
|
||||
isActive: false,
|
||||
isDelete: true,
|
||||
})
|
||||
.andWhere(
|
||||
_data.root != undefined && _data.root != null
|
||||
? _data.root[0] != null
|
||||
? `current_holders.orgRootId IN (:...root)`
|
||||
: `current_holders.orgRootId is null`
|
||||
: "1=1",
|
||||
{ root: _data.root },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child1 != undefined && _data.child1 != null
|
||||
? _data.child1[0] != null
|
||||
? `current_holders.orgChild1Id IN (:...child1)`
|
||||
: `current_holders.orgChild1Id is null`
|
||||
: "1=1",
|
||||
{ child1: _data.child1 },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child2 != undefined && _data.child2 != null
|
||||
? _data.child2[0] != null
|
||||
? `current_holders.orgChild2Id IN (:...child2)`
|
||||
: `current_holders.orgChild2Id is null`
|
||||
: "1=1",
|
||||
{ child2: _data.child2 },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child3 != undefined && _data.child3 != null
|
||||
? _data.child3[0] != null
|
||||
? `current_holders.orgChild3Id IN (:...child3)`
|
||||
: `current_holders.orgChild3Id is null`
|
||||
: "1=1",
|
||||
{ child3: _data.child3 },
|
||||
)
|
||||
.andWhere(
|
||||
_data.child4 != undefined && _data.child4 != null
|
||||
? _data.child4[0] != null
|
||||
? `current_holders.orgChild4Id IN (:...child4)`
|
||||
: `current_holders.orgChild4Id is null`
|
||||
: "1=1",
|
||||
{ child4: _data.child4 },
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.orWhere(body.keyword ? queryLike : "1=1", { keyword: `%${body.keyword}%` });
|
||||
|
|
|
|||
59
src/seed/README.md
Normal file
59
src/seed/README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Seed — Mock data สำหรับ dev/test (`src/seed/`)
|
||||
|
||||
> ⚠️ **DESTRUCTIVE** — seed หลายตัวล้างข้อมูลจริง (org/pos/profile) เพื่อใส่ mock แทน
|
||||
> **ห้ามรันบน production/staging** — เฉพาะ DB test เท่านั้น
|
||||
|
||||
## seed คืออะไร
|
||||
|
||||
ชุด mock data สำหรับทำ DB test ให้ dev เริ่มงานได้ทันทีโดยไม่มีข้อมูลจริง (PII) ปน:
|
||||
- โครงสร้างองค์กร mock: สำนักA → หน่วยงานA → ฝ่ายA → กลุ่มงานA (revision "โครงสร้าง 2569")
|
||||
- ตำแหน่ง mock: 20 posMaster (ข้าราชการ) + 10 employeePosMaster (ลูกจ้าง)
|
||||
- พนักงาน mock: 20 Profile + 10 ProfileEmployee (ชื่อไทย + citizenId ผ่าน checksum + birthDate)
|
||||
- ผูกคนเกาะตำแหน่ง (current_holderId)
|
||||
|
||||
## แยกจาก migration หลัก
|
||||
|
||||
seed **ไม่ได้อยู่ใน migration pipeline หลัก** (`src/migration/`) → `npm run migration:run` (หลัก) **จะไม่เจอ seed** → seed จะไม่ทำลายข้อมูลบน env อื่น
|
||||
|
||||
seed มี DataSource + runner ของตัวเอง → รันด้วย `npm run seed:*`
|
||||
|
||||
## คำสั่ง
|
||||
|
||||
```sh
|
||||
npm run seed:show # ดูสถานะ seed (applied/pending)
|
||||
npm run seed:run # รัน seed ที่ยังไม่ applied
|
||||
npm run seed:revert # revert seed ตัวสุดท้าย (เรียก down())
|
||||
```
|
||||
|
||||
ใช้ DataSource `src/seed/data-source.ts` (glob `src/seed/migrations/**`)
|
||||
|
||||
## โครงสร้างไฟล์
|
||||
|
||||
```
|
||||
src/seed/
|
||||
├── README.md (ไฟล์นี้)
|
||||
├── data-source.ts (SeedDataSource — แยกจาก src/database/data-source.ts)
|
||||
├── mockData.ts (helper: ชื่อไทย + genValidCitizenId checksum + deterministic UUID + genBirthDate)
|
||||
└── migrations/
|
||||
├── 1782100000001-seed_org_revision_2569.ts (CP3: orgRevision → 1, "โครงสร้าง 2569", ล้าง org/pos เดิม)
|
||||
├── 1782100000002-seed_mock_org_structure.ts (CP4: org mock สำนักA/หน่วยงานA/ฝ่ายA/กลุ่มงานA)
|
||||
├── 1782100000003-seed_mock_positions.ts (CP5a: 20 posMaster + 10 employeePosMaster)
|
||||
├── 1782100000004-seed_mock_profiles.ts (CP5b: ล้าง profile เดิม + 20 Profile + 10 ProfileEmployee)
|
||||
└── 1782100000005-seed_link_holders.ts (CP5c: ผูกคนเกาะตำแหน่ง current_holderId)
|
||||
```
|
||||
|
||||
## ค่า mock
|
||||
|
||||
- **citizenId**: สุ่ม 13 หลักผ่าน Thai checksum จริง + ห้ามตรงของเดิม (snapshot block list ใน CP5b ก่อนลบ)
|
||||
- **birthDate**: deterministic `1970+(i%30)` → ใช้คำนวณ password Keycloak ใน CP7 (`ddMMyyyy` พ.ศ.)
|
||||
- **UUID**: deterministic (`mockUUID(prefix, i)`) เพื่อ idempotent + FK อ้างข้าม migration ได้
|
||||
- **audit columns**: `createdUserId/lastUpdateUserId = 00000000-...`, `createdFullName/lastUpdateFullName = 'System Administrator'` (ตาม `EntityBase`)
|
||||
|
||||
## ⚠️ ข้อควรระวัง
|
||||
|
||||
1. **TypeORM quirk**: `1782100000003-seed_mock_positions.ts` (CP5a) INSERT SELECT WHERE NOT EXISTS อาจไม่ execute ใน transaction ของ TypeORM version นี้ → ถ้า `seed:run` แล้ว posMaster/employeePosMaster ว่าง ให้ seed ตรงผ่าน script (ดู plan CP5a note)
|
||||
2. **ลำดับ FK**: CP3→CP4→CP5a→CP5b→CP5c (ตาม timestamp 178210000000{1..5}) — seed ต้องรันตามลำดับ
|
||||
3. **CP6/CP7 (Keycloak) defer**: seed นี้ยังไม่ผูก Keycloak — mock profile `keycloak=NULL` (login ไม่ได้). ดู plan สำหรับ CP7
|
||||
|
||||
## เอกสารที่เกี่ยวข้อง
|
||||
- `.claude/plans/plan_cleanup-mock-seed.md` — **ไฟล์เดียวจบ**: สรุปตาราง DB (Part A) + plan checklist CP0-CP7 (Part B) + drop script SQL (Part C)
|
||||
34
src/seed/data-source.ts
Normal file
34
src/seed/data-source.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import "dotenv/config";
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
/**
|
||||
* Seed DataSource — แยกจาก migration หลัก (src/database/data-source.ts)
|
||||
*
|
||||
* glob ชี้เฉพาะ src/seed/migrations/** ดังนั้น `npm run migration:run` (migration หลัก)
|
||||
* จะไม่เจอ seed migration เหล่านี้ → seed mock จะไม่ทำลายข้อมูลบน env อื่น (prod/staging)
|
||||
*
|
||||
* รัน seed ผ่าน: `npm run seed:run` / `seed:show` / `seed:revert`
|
||||
*
|
||||
* ⚠️ seed migration หลายตัว DESTRUCTIVE (CP3 ล้าง org/pos, CP5b ล้าง profile)
|
||||
* ห้ามรันบน production — เฉพาะ DB test (DB_HOST=192.168.1.68)
|
||||
*/
|
||||
export const SeedDataSource = new DataSource({
|
||||
type: "mysql",
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
port: +(process.env.DB_PORT || 3306),
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
connectorPackage: "mysql2",
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
entities: [],
|
||||
migrations:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? ["src/seed/migrations/**/*.ts"]
|
||||
: ["dist/seed/migrations/**/*{.ts,.js}"],
|
||||
extra: {
|
||||
timezone: "+07:00", // Bangkok timezone (UTC+7)
|
||||
},
|
||||
});
|
||||
76
src/seed/migrations/1782100000001-seed_org_revision_2569.ts
Normal file
76
src/seed/migrations/1782100000001-seed_org_revision_2569.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import { AUDIT, genRevisionUUID } from "../mockData";
|
||||
|
||||
/**
|
||||
* CP3 — Curate orgRevision: wipe all existing org/position structure and seed
|
||||
* a single current revision "โครงสร้าง 2569". Mock org tree + positions are
|
||||
* inserted by later migrations (CP4/CP5a).
|
||||
*
|
||||
* DESTRUCTIVE — relies on CP1 mysqldump backup for rollback.
|
||||
*/
|
||||
export class SeedOrgRevision25691782100000001 implements MigrationInterface {
|
||||
public name = "SeedOrgRevision25691782100000001";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const REV = genRevisionUUID(0);
|
||||
const audit = [AUDIT.createdUserId, AUDIT.lastUpdateUserId, AUDIT.createdFullName, AUDIT.lastUpdateFullName];
|
||||
|
||||
await queryRunner.query("SET FOREIGN_KEY_CHECKS = 0");
|
||||
|
||||
// Wipe all org/position tables (child → parent order). FK constraints are
|
||||
// mostly stripped in this repo, but FK_CHECKS=0 guards the few that remain.
|
||||
// Some tables may already be gone (e.g. positionOfficer was dropped in the
|
||||
// cleanup step) — only DELETE tables that still exist.
|
||||
const wipeTables = [
|
||||
"posMasterHistory",
|
||||
"posMasterEmployeeHistory",
|
||||
"posMasterEmployeeTempHistory",
|
||||
"posMasterAct",
|
||||
"posMasterAssign",
|
||||
"positionSalaryEditHistory",
|
||||
"position",
|
||||
"employeePosition",
|
||||
"posMaster",
|
||||
"employeePosMaster",
|
||||
"employeeTempPosMaster",
|
||||
"orgChild4",
|
||||
"orgChild3",
|
||||
"orgChild2",
|
||||
"orgChild1",
|
||||
"permissionOrg", // FK -> orgRoot
|
||||
"orgRoot",
|
||||
"orgRevision",
|
||||
];
|
||||
const existingRows = (await queryRunner.query(
|
||||
`SELECT TABLE_NAME AS t FROM information_schema.tables
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME IN (${wipeTables.map(() => "?").join(",")})`,
|
||||
wipeTables,
|
||||
)) as Array<{ t: string }>;
|
||||
const existing = new Set(existingRows.map((r) => r.t));
|
||||
for (const t of wipeTables) {
|
||||
if (existing.has(t)) {
|
||||
await queryRunner.query(`DELETE FROM \`${t}\``);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed single current revision "โครงสร้าง 2569" (idempotent).
|
||||
await queryRunner.query(
|
||||
`INSERT INTO orgRevision
|
||||
(id, orgRevisionName, orgRevisionIsCurrent, orgRevisionIsDraft, isLock,
|
||||
orgRevisionCreatedAt, createdAt, lastUpdatedAt, createdUserId, lastUpdateUserId, createdFullName, lastUpdateFullName)
|
||||
SELECT ?, 'โครงสร้าง 2569', true, false, false, NOW(), NOW(), NOW(), ?, ?, ?, ?
|
||||
WHERE NOT EXISTS (SELECT 1 FROM orgRevision WHERE id = ?)`,
|
||||
[REV, ...audit, REV],
|
||||
);
|
||||
|
||||
await queryRunner.query("SET FOREIGN_KEY_CHECKS = 1");
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Best-effort: remove only the seeded revision row. Full restore = CP1 backup.
|
||||
await queryRunner.query("SET FOREIGN_KEY_CHECKS = 0");
|
||||
await queryRunner.query(`DELETE FROM orgRevision WHERE id = ?`, [genRevisionUUID(0)]);
|
||||
await queryRunner.query("SET FOREIGN_KEY_CHECKS = 1");
|
||||
}
|
||||
}
|
||||
67
src/seed/migrations/1782100000002-seed_mock_org_structure.ts
Normal file
67
src/seed/migrations/1782100000002-seed_mock_org_structure.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import {
|
||||
AUDIT,
|
||||
genRevisionUUID,
|
||||
genRootUUID,
|
||||
genChild1UUID,
|
||||
genChild2UUID,
|
||||
genChild3UUID,
|
||||
} from "../mockData";
|
||||
|
||||
/**
|
||||
* CP4 — Seed mock org structure: สำนักA (DEPARTMENT) → หน่วยงานA (OFFICE) →
|
||||
* ฝ่ายA (DIVISION) → กลุ่มงานA (SECTION), all under revision 2569.
|
||||
*/
|
||||
export class SeedMockOrgStructure1782100000002 implements MigrationInterface {
|
||||
public name = "SeedMockOrgStructure1782100000002";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const REV = genRevisionUUID(0);
|
||||
const ROOT = genRootUUID(0);
|
||||
const C1 = genChild1UUID(0);
|
||||
const C2 = genChild2UUID(0);
|
||||
const C3 = genChild3UUID(0);
|
||||
const audit = [AUDIT.createdUserId, AUDIT.lastUpdateUserId, AUDIT.createdFullName, AUDIT.lastUpdateFullName];
|
||||
const auditCols = "createdAt, lastUpdatedAt, createdUserId, lastUpdateUserId, createdFullName, lastUpdateFullName";
|
||||
const auditPlace = "NOW(), NOW(), ?, ?, ?, ?";
|
||||
|
||||
// orgRoot: สำนักA (DEPARTMENT)
|
||||
await queryRunner.query(
|
||||
`INSERT INTO orgRoot (id, orgRootName, orgRootShortName, orgRootRank, orgRootOrder, orgRevisionId, ${auditCols})
|
||||
SELECT ?, 'สำนักA', 'สน.A', 'DEPARTMENT', 1, ?, ${auditPlace}
|
||||
WHERE NOT EXISTS (SELECT 1 FROM orgRoot WHERE id = ?)`,
|
||||
[ROOT, REV, ...audit, ROOT],
|
||||
);
|
||||
|
||||
// orgChild1: หน่วยงานA (OFFICE) — FK root+rev
|
||||
await queryRunner.query(
|
||||
`INSERT INTO orgChild1 (id, orgChild1Name, orgChild1ShortName, orgChild1Rank, orgChild1Order, orgRootId, orgRevisionId, ${auditCols})
|
||||
SELECT ?, 'หน่วยงานA', 'น.A', 'OFFICE', 1, ?, ?, ${auditPlace}
|
||||
WHERE NOT EXISTS (SELECT 1 FROM orgChild1 WHERE id = ?)`,
|
||||
[C1, ROOT, REV, ...audit, C1],
|
||||
);
|
||||
|
||||
// orgChild2: ฝ่ายA (DIVISION) — FK root+c1+rev
|
||||
await queryRunner.query(
|
||||
`INSERT INTO orgChild2 (id, orgChild2Name, orgChild2ShortName, orgChild2Rank, orgChild2Order, orgRootId, orgChild1Id, orgRevisionId, ${auditCols})
|
||||
SELECT ?, 'ฝ่ายA', 'ฝ.A', 'DIVISION', 1, ?, ?, ?, ${auditPlace}
|
||||
WHERE NOT EXISTS (SELECT 1 FROM orgChild2 WHERE id = ?)`,
|
||||
[C2, ROOT, C1, REV, ...audit, C2],
|
||||
);
|
||||
|
||||
// orgChild3: กลุ่มงานA (SECTION) — FK root+c1+c2+rev
|
||||
await queryRunner.query(
|
||||
`INSERT INTO orgChild3 (id, orgChild3Name, orgChild3ShortName, orgChild3Rank, orgChild3Order, orgRootId, orgChild1Id, orgChild2Id, orgRevisionId, ${auditCols})
|
||||
SELECT ?, 'กลุ่มงานA', 'กล.A', 'SECTION', 1, ?, ?, ?, ?, ${auditPlace}
|
||||
WHERE NOT EXISTS (SELECT 1 FROM orgChild3 WHERE id = ?)`,
|
||||
[C3, ROOT, C1, C2, REV, ...audit, C3],
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DELETE FROM orgChild3 WHERE id = ?`, [genChild3UUID(0)]);
|
||||
await queryRunner.query(`DELETE FROM orgChild2 WHERE id = ?`, [genChild2UUID(0)]);
|
||||
await queryRunner.query(`DELETE FROM orgChild1 WHERE id = ?`, [genChild1UUID(0)]);
|
||||
await queryRunner.query(`DELETE FROM orgRoot WHERE id = ?`, [genRootUUID(0)]);
|
||||
}
|
||||
}
|
||||
56
src/seed/migrations/1782100000003-seed_mock_positions.ts
Normal file
56
src/seed/migrations/1782100000003-seed_mock_positions.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import {
|
||||
AUDIT,
|
||||
genRevisionUUID,
|
||||
genRootUUID,
|
||||
genChild1UUID,
|
||||
genChild2UUID,
|
||||
genChild3UUID,
|
||||
genPosUUID,
|
||||
genEmpPosUUID,
|
||||
} from "../mockData";
|
||||
|
||||
/**
|
||||
* CP5a — Seed mock positions: 20 posMaster (ข้าราชการ) + 10 employeePosMaster
|
||||
* (ลูกจ้าง) under กลุ่มงานA (orgChild3). current_holderId = NULL here, linked
|
||||
* in CP5c.
|
||||
*/
|
||||
export class SeedMockPositions1782100000003 implements MigrationInterface {
|
||||
public name = "SeedMockPositions1782100000003";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const REV = genRevisionUUID(0);
|
||||
const ROOT = genRootUUID(0);
|
||||
const C1 = genChild1UUID(0);
|
||||
const C2 = genChild2UUID(0);
|
||||
const C3 = genChild3UUID(0);
|
||||
const audit = [AUDIT.createdUserId, AUDIT.lastUpdateUserId, AUDIT.createdFullName, AUDIT.lastUpdateFullName];
|
||||
const insertPos = async (table: string, uuid: string, no: number) => {
|
||||
await queryRunner.query(
|
||||
`INSERT INTO \`${table}\`
|
||||
(id, posMasterNo, orgRootId, orgChild1Id, orgChild2Id, orgChild3Id,
|
||||
orgRevisionId, current_holderId, next_holderId,
|
||||
createdAt, lastUpdatedAt, createdUserId, lastUpdateUserId, createdFullName, lastUpdateFullName)
|
||||
SELECT ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NOW(), NOW(), ?, ?, ?, ?
|
||||
WHERE NOT EXISTS (SELECT 1 FROM \`${table}\` WHERE id = ?)`,
|
||||
[uuid, no, ROOT, C1, C2, C3, REV, ...audit, uuid],
|
||||
);
|
||||
};
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await insertPos("posMaster", genPosUUID(i), i + 1);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await insertPos("employeePosMaster", genEmpPosUUID(i), i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await queryRunner.query(`DELETE FROM posMaster WHERE id = ?`, [genPosUUID(i)]);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await queryRunner.query(`DELETE FROM employeePosMaster WHERE id = ?`, [genEmpPosUUID(i)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/seed/migrations/1782100000004-seed_mock_profiles.ts
Normal file
121
src/seed/migrations/1782100000004-seed_mock_profiles.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import {
|
||||
AUDIT,
|
||||
genProfileUUID,
|
||||
genEmpProfileUUID,
|
||||
genValidCitizenId,
|
||||
genBirthDate,
|
||||
mockName,
|
||||
MOCK_GENDERS,
|
||||
MOCK_RELIGIONS,
|
||||
MOCK_BLOOD,
|
||||
} from "../mockData";
|
||||
|
||||
/**
|
||||
* CP5b — Replace real profile/profileEmployee data with 30 mock people
|
||||
* (20 Profile / 10 ProfileEmployee). Mock citizenId passes the Thai checksum,
|
||||
* never collides with any previously-existing citizenId (block list).
|
||||
*
|
||||
* DESTRUCTIVE — relies on CP1 mysqldump backup for rollback.
|
||||
*
|
||||
* Per user: clear profile.keycloak / profileEmployee.keycloak BEFORE deleting,
|
||||
* so the Keycloak orphan-clear step (CP6) treats old KC users as orphans.
|
||||
*/
|
||||
export class SeedMockProfiles1782100000004 implements MigrationInterface {
|
||||
public name = "SeedMockProfiles1782100000004";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. Snapshot existing citizenIds → block list (mock ids must not collide)
|
||||
const realIdRows = (await queryRunner.query(
|
||||
`SELECT citizenId FROM profile WHERE citizenId IS NOT NULL AND citizenId <> ''
|
||||
UNION
|
||||
SELECT citizenId FROM profileEmployee WHERE citizenId IS NOT NULL AND citizenId <> ''`,
|
||||
)) as Array<{ citizenId: string }>;
|
||||
const blockList = new Set(realIdRows.map((r) => r.citizenId));
|
||||
|
||||
await queryRunner.query("SET FOREIGN_KEY_CHECKS = 0");
|
||||
|
||||
// 2. Clear Keycloak pointers BEFORE delete (per user requirement)
|
||||
await queryRunner.query(`UPDATE profile SET keycloak = NULL`);
|
||||
await queryRunner.query(`UPDATE profileEmployee SET keycloak = NULL`);
|
||||
|
||||
// 3. Detach any holder references
|
||||
await queryRunner.query(`UPDATE posMaster SET current_holderId = NULL, next_holderId = NULL`);
|
||||
await queryRunner.query(`UPDATE employeePosMaster SET current_holderId = NULL, next_holderId = NULL`);
|
||||
|
||||
// 4. Delete child tables that FK profile/profileEmployee (dynamic discovery)
|
||||
const childRows = (await queryRunner.query(
|
||||
`SELECT DISTINCT TABLE_NAME AS t FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND REFERENCED_TABLE_NAME IN ('profile','profileEmployee')`,
|
||||
)) as Array<{ t: string }>;
|
||||
for (const c of childRows) {
|
||||
await queryRunner.query(`DELETE FROM \`${c.t}\``);
|
||||
}
|
||||
|
||||
// 5. Delete real profile data
|
||||
await queryRunner.query(`DELETE FROM profile`);
|
||||
await queryRunner.query(`DELETE FROM profileEmployee`);
|
||||
|
||||
const audit = [AUDIT.createdUserId, AUDIT.lastUpdateUserId, AUDIT.createdFullName, AUDIT.lastUpdateFullName];
|
||||
|
||||
// 6. Insert 20 mock Profile (ข้าราชการ)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const nm = mockName(i);
|
||||
const cid = genValidCitizenId(i, blockList);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO profile
|
||||
(id, citizenId, firstName, lastName, prefix, gender, religion, bloodGroup,
|
||||
birthDate, isActive, isDelete, isProbation, isLeave, isRetirement, statusCheckEdit, keycloak,
|
||||
createdAt, lastUpdatedAt, createdUserId, lastUpdateUserId, createdFullName, lastUpdateFullName)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, true, false, false, false, false, 'PENDING', NULL, NOW(), NOW(), ?, ?, ?, ?)`,
|
||||
[
|
||||
genProfileUUID(i),
|
||||
cid,
|
||||
nm.firstName,
|
||||
nm.lastName,
|
||||
nm.prefix,
|
||||
MOCK_GENDERS[i % MOCK_GENDERS.length],
|
||||
MOCK_RELIGIONS[i % MOCK_RELIGIONS.length],
|
||||
MOCK_BLOOD[i % MOCK_BLOOD.length],
|
||||
genBirthDate(i),
|
||||
...audit,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Insert 10 mock ProfileEmployee (ลูกจ้าง, alternate PERM/TEMP)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const idx = i + 20;
|
||||
const nm = mockName(idx);
|
||||
const cid = genValidCitizenId(idx, blockList);
|
||||
const empClass = i % 2 === 0 ? "PERM" : "TEMP";
|
||||
await queryRunner.query(
|
||||
`INSERT INTO profileEmployee
|
||||
(id, citizenId, firstName, lastName, prefix, gender, religion, bloodGroup,
|
||||
birthDate, employeeClass, isActive, isDelete, isLeave, statusCheckEdit, keycloak,
|
||||
createdAt, lastUpdatedAt, createdUserId, lastUpdateUserId, createdFullName, lastUpdateFullName)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, true, false, false, 'PENDING', NULL, NOW(), NOW(), ?, ?, ?, ?)`,
|
||||
[
|
||||
genEmpProfileUUID(i),
|
||||
cid,
|
||||
nm.firstName,
|
||||
nm.lastName,
|
||||
nm.prefix,
|
||||
MOCK_GENDERS[idx % MOCK_GENDERS.length],
|
||||
MOCK_RELIGIONS[idx % MOCK_RELIGIONS.length],
|
||||
MOCK_BLOOD[idx % MOCK_BLOOD.length],
|
||||
genBirthDate(idx),
|
||||
empClass,
|
||||
...audit,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query("SET FOREIGN_KEY_CHECKS = 1");
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
// No safe in-place rollback — restore from CP1 mysqldump backup.
|
||||
}
|
||||
}
|
||||
39
src/seed/migrations/1782100000005-seed_link_holders.ts
Normal file
39
src/seed/migrations/1782100000005-seed_link_holders.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import {
|
||||
genPosUUID,
|
||||
genEmpPosUUID,
|
||||
genProfileUUID,
|
||||
genEmpProfileUUID,
|
||||
} from "../mockData";
|
||||
|
||||
/**
|
||||
* CP5c — Link mock people to mock positions: set current_holderId so the 30
|
||||
* mock people are attached to the mock org structure.
|
||||
* posMaster[i].current_holderId = profile[i] (0..19)
|
||||
* employeePosMaster[i].current_holderId = profileEmployee[i] (0..9)
|
||||
*/
|
||||
export class SeedLinkHolders1782100000005 implements MigrationInterface {
|
||||
public name = "SeedLinkHolders1782100000005";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await queryRunner.query(
|
||||
`UPDATE posMaster SET current_holderId = ? WHERE id = ?`,
|
||||
[genProfileUUID(i), genPosUUID(i)],
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await queryRunner.query(
|
||||
`UPDATE employeePosMaster SET current_holderId = ? WHERE id = ?`,
|
||||
[genEmpProfileUUID(i), genEmpPosUUID(i)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const posIds = Array.from({ length: 20 }, (_, i) => `'${genPosUUID(i)}'`).join(",");
|
||||
const empPosIds = Array.from({ length: 10 }, (_, i) => `'${genEmpPosUUID(i)}'`).join(",");
|
||||
await queryRunner.query(`UPDATE posMaster SET current_holderId = NULL WHERE id IN (${posIds})`);
|
||||
await queryRunner.query(`UPDATE employeePosMaster SET current_holderId = NULL WHERE id IN (${empPosIds})`);
|
||||
}
|
||||
}
|
||||
111
src/seed/mockData.ts
Normal file
111
src/seed/mockData.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Mock data helpers for seed migrations.
|
||||
// Filename starts with underscore "_" so TypeORM migration runner skips it
|
||||
// (only files exporting a class implementing MigrationInterface are loaded).
|
||||
//
|
||||
// All generators are DETERMINISTIC (same index -> same value) so seed migrations
|
||||
// stay idempotent across re-runs, and foreign keys between mock rows can be
|
||||
// computed up-front (e.g. PosMaster.current_holderId <- genProfileUUID(i)).
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audit columns — must match src/entities/base/Base.ts defaults
|
||||
// ---------------------------------------------------------------------------
|
||||
export const AUDIT = {
|
||||
createdUserId: "00000000-0000-0000-0000-000000000000",
|
||||
lastUpdateUserId: "00000000-0000-0000-0000-000000000000",
|
||||
createdFullName: "System Administrator",
|
||||
lastUpdateFullName: "System Administrator",
|
||||
} as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deterministic UUID generator (valid UUID format, no external dep)
|
||||
// 8 hex from djb2 hash of prefix | 4 | 4 (v4-ish) | 4 | 12 hex from idx
|
||||
// unique per (prefix, idx); different prefixes -> different leading 8 hex
|
||||
// ---------------------------------------------------------------------------
|
||||
export function mockUUID(prefix: string, idx: number): string {
|
||||
let h = 5381;
|
||||
for (let k = 0; k < prefix.length; k++) h = ((h << 5) + h + prefix.charCodeAt(k)) >>> 0;
|
||||
const head = h.toString(16).padStart(8, "0").slice(-8);
|
||||
const tail = (idx >>> 0).toString(16).padStart(12, "0").slice(-12);
|
||||
return `${head}-0000-4000-8000-${tail}`;
|
||||
}
|
||||
|
||||
export const genRevisionUUID = (i = 0) => mockUUID("orgRevision", i);
|
||||
export const genRootUUID = (i: number) => mockUUID("orgRoot", i);
|
||||
export const genChild1UUID = (i: number) => mockUUID("orgChild1", i);
|
||||
export const genChild2UUID = (i: number) => mockUUID("orgChild2", i);
|
||||
export const genChild3UUID = (i: number) => mockUUID("orgChild3", i);
|
||||
export const genChild4UUID = (i: number) => mockUUID("orgChild4", i);
|
||||
export const genPosUUID = (i: number) => mockUUID("posMaster", i);
|
||||
export const genEmpPosUUID = (i: number) => mockUUID("employeePosMaster", i);
|
||||
export const genProfileUUID = (i: number) => mockUUID("profile", i);
|
||||
export const genEmpProfileUUID = (i: number) => mockUUID("profileEmployee", i);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Thai citizen ID (13 digits) — passes real checksum, but never collides with
|
||||
// existing IDs in `blockList` (snapshot of real citizenIds taken before delete).
|
||||
// Deterministic pseudo-random from idx so re-runs are stable.
|
||||
// ---------------------------------------------------------------------------
|
||||
export function genValidCitizenId(idx: number, blockList: Set<string>): string {
|
||||
for (let attempt = 0; attempt < 100000; attempt++) {
|
||||
// LCG-style 12-digit base, seeded by (idx, attempt) -> looks random, reproducible
|
||||
const seed = (idx * 2654435761 + attempt * 40503 + 123456789012) >>> 0;
|
||||
const seed2 = (seed ^ (idx * 2246822519)) >>> 0;
|
||||
const base =
|
||||
(seed % 100000000).toString().padStart(8, "0") +
|
||||
(seed2 % 10000).toString().padStart(4, "0"); // 12 digits
|
||||
let sum = 0;
|
||||
for (let k = 0; k < 12; k++) sum += parseInt(base[k], 10) * (13 - k);
|
||||
const check = (11 - (sum % 11)) % 10;
|
||||
const id = base + check;
|
||||
if (!blockList.has(id)) return id;
|
||||
}
|
||||
throw new Error(`genValidCitizenId: cannot find non-colliding id for idx=${idx}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Thai mock names
|
||||
// ---------------------------------------------------------------------------
|
||||
const FIRST_M = [
|
||||
"สมชาย", "วิชาญ", "ประยุทธ", "อนุชา", "ธีรพงษ์", "นิรันดร", "อดุลย์", "เกริก",
|
||||
"ภาคิน", "วีระ", "สมศักดิ์", "กิตติ", "ชัยรัตน์", "ปิยะ", "ธนภัทร",
|
||||
];
|
||||
const FIRST_F = [
|
||||
"สมหญิง", "มาลี", "ปนัดดา", "อรุณี", "ภัทรา", "กัลยา", "วรรณี", "สุภาพร",
|
||||
"อัจฉรา", "นันทิดา", "พรทิพา", "จันทร์เพ็ญ", "อรทัย", "สมศรี", "ประภาพร",
|
||||
];
|
||||
const LAST = [
|
||||
"ใจดี", "รักไทย", "ทองคำ", "ศรีสุข", "แก้วใส", "สุขุม", "มั่นคง", "เจริญสุข",
|
||||
"อยู่สบาย", "ทรงศักดิ์", "บุญมี", "วงศ์ไพศาล", "เกียรติศักดิ์", "ชัยมงคล",
|
||||
"พงษ์ไพจิตร", "รัตนชัย", "สีลา", "นวลละออง", "ตั้งตรงจิต", "อินทรพงศ์",
|
||||
];
|
||||
|
||||
export interface MockName {
|
||||
prefix: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
export function mockName(i: number): MockName {
|
||||
const male = i % 2 === 0; // alternate gender
|
||||
const firstName = male ? FIRST_M[i % FIRST_M.length] : FIRST_F[i % FIRST_F.length];
|
||||
const lastName = LAST[(i * 7) % LAST.length];
|
||||
const prefix = male ? "นาย" : i % 3 === 0 ? "นาง" : "นางสาว";
|
||||
return { prefix, firstName, lastName };
|
||||
}
|
||||
|
||||
// Mock demographic master values (Profile stores these as plain strings, not FK)
|
||||
export const MOCK_GENDERS = ["ชาย", "หญิง"] as const;
|
||||
export const MOCK_RELIGIONS = ["พุทธ", "คริสต์", "อิสลาม"] as const;
|
||||
export const MOCK_BLOOD = ["O", "A", "B", "AB"] as const;
|
||||
|
||||
// Deterministic birthDate (datetime string) for mock people.
|
||||
// CP7 derives the Keycloak password from this via ddMM(yyyy+543) format,
|
||||
// matching UserController.createUserImport (L1063-1071).
|
||||
export function genBirthDate(i: number): string {
|
||||
const year = 1970 + (i % 30); // 1970-1999
|
||||
const month = (i % 12) + 1; // 1-12
|
||||
const day = (i % 28) + 1; // 1-28 (safe for all months)
|
||||
const mm = month.toString().padStart(2, "0");
|
||||
const dd = day.toString().padStart(2, "0");
|
||||
return `${year}-${mm}-${dd} 00:00:00`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue