Plan + สรุปตาราง + Drop Script — Cleanup & Mock Seed hrms_organization
This commit is contained in:
parent
d3e1707cc2
commit
8201ac51af
11 changed files with 836 additions and 3 deletions
|
|
@ -11,6 +11,9 @@
|
||||||
"build": "tsoa spec-and-routes && tsc",
|
"build": "tsoa spec-and-routes && tsc",
|
||||||
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts",
|
"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",
|
"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": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage"
|
"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';
|
||||||
|
```
|
||||||
|
|
@ -106,9 +106,13 @@ import { ProvinceMaster } from "../entities/ProvinceMaster";
|
||||||
import { SubDistrictMaster } from "../entities/SubDistrictMaster";
|
import { SubDistrictMaster } from "../entities/SubDistrictMaster";
|
||||||
import { DistrictMaster } from "../entities/DistrictMaster";
|
import { DistrictMaster } from "../entities/DistrictMaster";
|
||||||
import { RequestWithUser } from "../middlewares/user";
|
import { RequestWithUser } from "../middlewares/user";
|
||||||
@Route("api/v1/org/upload")
|
// ⛔ DISABLED 2026-07-21 — MIS tables ทั้งหมดจะถูก DROP (ดู .claude/docs/2026-07-21-drop-unused-tables.sql)
|
||||||
@Tags("UPLOAD")
|
// controller นี้เรียกใช้ mis entities (Profile*, EMPLOYEE, HR_*, ...) ผ่าน getRepository แทบทุก endpoint
|
||||||
@Security("bearerAuth")
|
// ถ้าปล่อยไว้ → 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 {
|
export class ImportDataController extends Controller {
|
||||||
private profileEducationRepo = AppDataSource.getRepository(ProfileEducation);
|
private profileEducationRepo = AppDataSource.getRepository(ProfileEducation);
|
||||||
private profileDisciplineRepo = AppDataSource.getRepository(ProfileDiscipline);
|
private profileDisciplineRepo = AppDataSource.getRepository(ProfileDiscipline);
|
||||||
|
|
|
||||||
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