3 · Persistence
pms-api ใช้ TypeORM เป็น ORM ตัวเดียว เชื่อมต่อ MSSQL 4 connection (main / vtrc / workforce / workforceTemp) โดย entity ทั้งหมด 66 ไฟล์ผูกกับ connection main (database pms) ส่วน workforce และ workforceTemp เข้าถึงผ่าน EntityManager + query builder / parameterized query() โดยไม่มี entity ของตัวเอง — vtrc ประกาศไว้แต่ไม่พบการ inject ใช้งานจริง
ทุก claim ในบทนี้อ้างจาก canonical branch uat (ดู 01 Repository map และ 02 Period cycle สำหรับบริบทวงจรธุรกิจ)
TypeORM connections (4 ตัว)
ลงทะเบียนที่ pms-api/src/app.module.ts (branch: uat):59-82
TypeOrmModule.forRootAsync({
name: 'main',
useFactory: (configService) => configService.get('db'),
}),
TypeOrmModule.forRootAsync({
name: 'vtrc',
useFactory: (configService) => configService.get('vtrcDB'),
}),
TypeOrmModule.forRootAsync({
name: 'workforce',
useFactory: (configService) => configService.get('workforceDB'),
}),
TypeOrmModule.forRootAsync({
name: 'workforceTemp',
useFactory: (configService) => configService.get('workforceTempDB'),
}),ค่า config มาจาก pms-api/config/configuration.ts (branch: uat):8-68
| Connection | env prefix | database (UAT) | synchronize | autoLoadEntities | สถานะ |
|---|---|---|---|---|---|
main | DB_* | pms | NODE_ENV === 'dev' เท่านั้น | เหมือน synchronize | ใช้งานจริง — entity ทั้ง 66 ตัว |
workforce | WORKFORCE_DB_* | dbWorkforce | false เสมอ | false | ใช้งานจริง — 10 module inject |
workforceTemp | WORKFORCE_TEMP_DB_* | dbWorkforce_temp | false เสมอ | false | ใช้งานจริง — เฉพาะ module rcc |
vtrc | VTRC_DB_* | vtrc | false เสมอ | false | ประกาศไว้แต่ไม่พบ inject |
synchronize / autoLoadEntities ของ main เปิดเฉพาะ NODE_ENV === 'dev' ที่ pms-api/config/configuration.ts (branch: uat):15-16 — ใน UAT/prod ต้อง migrate schema ด้วยมือผ่านโฟลเดอร์ pms-api/sql/ (มีสคริปต์ PMS02–PMS15, hotfix/, phase2/, report/)
ทุก connection ตั้ง extra.trustServerCertificate: true และ encrypt: false (configuration.ts:19-22 และซ้ำในทุก block) — ยอมรับ self-signed cert และปิด encryption ของ TDS
logging เปิด ['error', 'query'] ทุก connection — ใน UAT จะ log SQL ทุกคำสั่งลง stdout (ผลกระทบ observability ดู 06 Scorecard OBS-PMS-02)
Base entity
ทุก entity ที่ transactional เกือบทั้งหมด extend BaseEntity ที่ pms-api/src/entities/base.ts (branch: uat):3-27
export class BaseEntity {
@PrimaryGeneratedColumn('increment', { name: 'id' })
id?: number;
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
createdAt?: Date;
@Column({ type: 'varchar', length: 500, nullable: true })
createdByName?: string;
@Column({ type: 'varchar', length: 50, nullable: true })
createdBy?: string;
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
updatedAt?: Date;
@Column({ type: 'varchar', length: 50, nullable: true })
updatedBy?: string;
@Column({ type: 'varchar', length: 500, nullable: true })
updatedByName?: string;
@Column({ type: 'bit', default: false, nullable: false })
isDeleted?: boolean;
}soft-delete ใช้ isDeleted = 1 ไม่ใช่ hard delete — query ส่วนใหญ่ filter isDeleted = 0 ผ่าน query builder
ข้อยกเว้นชื่อตารางที่ไม่ขึ้นต้น tb — authData, mDropdown, emailConfig, pmsAttachFile (ไม่มี prefix tb)
รูปแบบออกแบบหลัก — *History คู่กับตารางหลัก
แทบทุกตารางหลักมีตาราง *History คู่กัน (เช่น tbEvaluateForm ↔ tbEvaluateFormHistory, tbSalaryAdjustHd ↔ tbSalaryAdjustHdHistory, tbBudgetHd ↔ tbBudgetHdHistory, tbPeriod ↔ tbPeriodHistory, tbIndicatorHd ↔ tbIndicatorHdHistory) — เป็น audit-trail pattern ที่สม่ำเสมอกว่า benefit-api / recruitment-api มาก สอดคล้องกับธรรมชาติของระบบประเมินผล/ปรับเงินเดือนที่ถูกตรวจสอบย้อนหลังบ่อย
history มักถูก insert ใน service เดียวกันกับ update หลักผ่าน createQueryBuilder().insert().values(...) ภายใน transaction หรือหลัง save() — ดูตัวอย่างใน period.service.ts, approver.service.ts, evaluateUserAmount.service.ts
Entity catalog (66 ไฟล์) — จัดกลุ่มตาม domain
นับจาก ls src/entities/*.entity.ts บน branch uat = 66 (ตรงกับ 01 Repository map; เอกสารเก่าบางฉบับเขียน 67 — ไม่ตรง)
กลุ่ม Period + Notification
| Entity class | @Entity(name) | field สำคัญ | ความสัมพันธ์ |
|---|---|---|---|
PeriodEntity | tbPeriod | fiscalYear, status (draft/confirm), isInactive, ช่วงวันที่ 5 คู่ (settingGoal / checkData / updateResult / evaluation / feedback) + settingGoalSubmit, maximumStartdate | 1:N → PeriodExtendEntity, PeriodHistoryEntity |
PeriodExtendEntity | tbPeriodExtend | settingGoalStartExtend/End, buId/buCode/buName, extendRemark | N:1 → PeriodEntity ผ่าน PMSPeriodId |
PeriodHistoryEntity | tbPeriodHistory | audit ของการแก้/ขยาย period | N:1 → PeriodEntity |
ConfigNotificationEntity | tbConfigNotification | notiTypeCode, isEmail/isVTRC/isLine, target (isEvaluatee/isEvaluator/isApprover/isOrgUnitAdmin), isAdvanceNoti, advanceNotiDay | 1:N → ConfigNotificationHistoryEntity |
ConfigNotificationHistoryEntity | tbConfigNotificationHistory | ประวัติแก้ config แจ้งเตือน | N:1 → config |
หมายเหตุชื่อไฟล์ — ไฟล์ entity ชื่อ periodExtesnd.entity.ts (typo Extesnd) แต่ class คือ PeriodExtendEntity และตารางจริงคือ tbPeriodExtend (pms-api/src/entities/periodExtesnd.entity.ts (branch: uat):5-6) — typo ค้างที่ชื่อไฟล์เท่านั้น ไม่ใช่ชื่อตาราง
รายละเอียด field วันที่ของ period ดู 02 Period cycle
กลุ่ม Indicator (ตัวชี้วัด + น้ำหนัก + scale + เกรด)
| Entity class | @Entity(name) | บทบาท |
|---|---|---|
IndicatorTypeEntity | tbIndicatorType | ประเภทตัวชี้วัดกลาง |
IndicatorTypeHistoryEntity | tbIndicatorTypeHistory | history |
IndicatorHdEntity | tbIndicatorHd | header ตัวชี้วัด — yearStart/yearEnd, status, usageStatus, refReviseId, FK indicatorWeightHdId |
IndicatorHdHistoryEntity | tbIndicatorHdHistory | history |
IndicatorDtEntity | tbIndicatorDt | รายละเอียดย่อยของตัวชี้วัด |
IndicatorWeightHdEntity | tbIndicatorWeightHd | สัดส่วนน้ำหนัก header |
IndicatorWeightDtEntity | tbIndicatorWeightDt | รายละเอียดน้ำหนัก |
IndicatorWeightHistoryEntity | tbIndicatorWeightHistory | history |
RatingScaleEntity | tbRatingScale | scale การให้คะแนน (1–5, 1–4, ฯลฯ) |
RatingScaleHistoryEntity | tbRatingScaleHistory | history |
GradingEntity | tbGradingHd | นิยามเกรดผลงาน (A/B/C/…) |
GradingDtEntity | tbGradingDt | รายละเอียดเกรด |
GradingHistoryEntity | tbGradingHdHistory | history |
GradeLimitHdEntity | tbGradeLimitHd | ขีดจำกัดจำนวน/สัดส่วนคนต่อเกรด (forced distribution) |
GradeLimitDtEntity | tbGradeLimitDt | รายละเอียด limit |
GradeLimitHistoryEntity | tbGradeLimitHistory | history |
ความสัมพันธ์หลักของ indicator — pms-api/src/entities/indicatorHd.entity.ts (branch: uat):9-61
@Entity('tbIndicatorHd')
export class IndicatorHdEntity extends BaseEntity {
@OneToMany(() => IndicatorDtEntity, (att) => att.indicatorHd, { cascade: true })
indicatorDt?: IndicatorDtEntity[];
@ManyToOne(() => IndicatorTypeEntity, (doc) => doc.indicatorHd, { cascade: true })
indicatorType?: IndicatorTypeEntity;
@OneToOne(() => IndicatorWeightHdEntity, { eager: false })
@JoinColumn({ name: 'indicatorWeightHdId' })
indicatorWeightHd?: IndicatorWeightHdEntity;
}กลุ่ม Evaluate (ประเมินผลจริง)
| Entity class | @Entity(name) | บทบาท | ความสัมพันธ์ |
|---|---|---|---|
EvaluateUserAmountEntity | tbEvaluateUserAmount | โควต้า/จำนวนผู้ถูกประเมินต่อหน่วยงานต่อปี | 1:N → users, history |
EvaluateUserAmountHistoryEntity | tbEvaluateUserAmountHistory | history โควต้า | N:1 |
EvaluateUserEntity | tbEvaluateUser | รายชื่อผู้ถูกประเมินต่อรอบ — มี empCode, identityCard, org fields, evaluator/agent | N:1 → amount; 1:1 → form |
EvaluateUserHistoryEntity | tbEvaluateUserHistory | history | N:1 |
EvaluateFormEntity | tbEvaluateForm | แบบประเมิน 1 ใบต่อ 1 คนต่อ 1 รอบ — มี PII (identityCard), คะแนน, สถานะ workflow | 1:N → Dt, Course, Learning, History, Summary, KpiRating, NoQuotaHistory |
EvaluateFormDtEntity | tbEvaluateFormDt | คะแนนรายตัวชี้วัด | N:1 → form; ผูก indicator |
EvaluateFormDtHistoryEntity | tbEvaluateFormDtHistory | history คะแนนรายข้อ | N:1 |
EvaluateFormHistoryEntity | tbEvaluateFormHistory | history สถานะแบบฟอร์ม | N:1 |
EvaluateFormKpiRatingEntity | tbEvaluateFormKpiRating | rating ราย KPI | N:1 → Dt |
EvaluateFormCourseEntity | tbEvaluateFormCourse | คอร์สแนะนำหลังประเมิน (จาก AI) | N:1 → form |
EvaluateFormLearningEntity | tbEvaluateFormLearning | แผนพัฒนารายบุคคล | N:1 → form |
EvaluateFormSummaryEntity | tbEvaluateFormSummary | สรุปคะแนนรวม | N:1 → form |
EvaluateSummaryResultEntity | tbEvaluateSummaryResult | ผลลัพธ์เกรดสุดท้าย | ผูกสรุปผล |
EvaluateFormNoQuotaHistoryEntity | tbEvaluateFormNoQuotaHistory | ประวัติกรณีไม่มีโควต้า | N:1 |
EvaluateFormEntity มี field PII ชัดเจนที่ pms-api/src/entities/evaluateForm.entity.ts (branch: uat):77-78 — identityCard ความยาว 17 ตัวอักษร (รูปแบบบัตรประชาชนไทย)
สายข้อมูลคะแนน → เงินเดือนที่ควรจำ:
evaluateFormSummary → evaluateSummaryResult (เกรด)
→ percentSalaryEmpLevelDt (เปอร์เซ็นต์ตามเกรด × ระดับ)
→ salaryAdjustDt (จำนวนเงินจริง)กลุ่ม Approver + Confirm
| Entity class | @Entity(name) | บทบาท |
|---|---|---|
ApproverHdEntity | tbApproverHd | สายอนุมัติหลักต่อปี — fiscalYear, status, isInactive |
ApproverDtEntity | tbApproverDt | ลำดับผู้อนุมัติรายคน |
ApproverHistoryEntity | tbApproverHistory | ประวัติเปลี่ยนผู้อนุมัติ (delegate) |
ConfirmQuotaHistoryEntity | tbConfirmQuotaHistory | ประวัติยืนยัน/ปฏิเสธโควต้าระดับหน่วยงาน |
OrgUnitConfirmHistoryEntity | tbOrgUnitConfirmHistory | ประวัติยืนยันผลระดับหน่วยงาน |
ApproverHdEntity ที่ pms-api/src/entities/approver.entity.ts (branch: uat):6-34 — cascade ลง approverDt และ approverHistory
กลุ่ม Salary (ใหญ่สุดใน repo)
| Entity class | @Entity(name) | บทบาท |
|---|---|---|
SalaryRangeHdEntity | tbSalaryRangeHd | ช่วงเงินเดือนตาม pay grade/position |
SalaryRangeDtEntity | tbSalaryRangeDT | รายละเอียดช่วง |
SalaryRangeHistoryEntity | tbSalaryRangeHistory | history |
PercentSalaryHDEntity | tbPercentSalaryHD | เปอร์เซ็นต์ปรับเงินเดือนตามเกรดผลประเมิน |
PercentSalaryEmpLevelEntity | tbPercentSalaryEmpLevel | ระดับพนักงาน |
PercentSalaryEmpLevelDtEntity | tbPercentSalaryEmpLevelDT | เปอร์เซ็นต์ × ระดับ × เกรด |
PercentSalaryHistoryEntity | tbPercentSalaryHDHistory | history |
SalaryAdjustHdEntity | tbSalaryAdjustHd | เอกสารปรับเงินเดือนหลัก — มี sendHrmAt, isHrmiGetData, budget summary |
SalaryAdjustHdHistoryEntity | tbSalaryAdjustHdHistory | history header |
SalaryAdjustBuEntity | tbSalaryAdjustBu | ระดับ business unit |
SalaryAdjustBuHistoryEntity | tbSalaryAdjustBuHistory | history BU |
SalaryAdjustDivEntity | tbSalaryAdjustDiv | ระดับ division |
SalaryAdjustDivHistoryEntity | tbSalaryAdjustDivHistory | history Div |
SalaryAdjustDtEntity | tbSalaryAdjustDt | รายละเอียดต่อคน (จำนวนเงินจริง) |
SalaryAdjustDtHistoryEntity | tbSalaryAdjustDtHistory | history Dt |
BudgetHdEntity | tbBudgetHd | งบประมาณปรับเงินเดือนที่ตั้งไว้ |
BudgetDtEntity | tbBudgetDT | รายละเอียดงบต่อหน่วยงาน + flag isAdjust / admin org unit |
BudgetHistoryEntity | tbBudgetHdHistory | history |
SalaryAdjustHdEntity ที่ pms-api/src/entities/salaryAdjustHd.entity.ts (branch: uat):6-67 มี field ส่งออก HRMI (sendHrmAt, sendHrmBy, isHrmiGetData, hrmiGetDataAt) — เชื่อมกับ module salaryAdjustToHRMI
BudgetHdEntity ที่ pms-api/src/entities/budget.entity.ts (branch: uat):8-45 — summaryBudgetStaff / summaryBudgetEmployee เป็นกรอบวงเงินรวม
กลุ่ม User / Auth / Master / Attach / Notify
| Entity class | @Entity(name) | บทบาท |
|---|---|---|
AuthDataEntity | authData | token store ของ user login (access + refresh) — pattern เดียวกับ recruitment-api |
PMSUserEntity | tbPMSUser | ผู้ใช้งานระบบ PMS + pmsPermission |
PMSUserDepAdminEntity | tbPMSUserDepAdmin | สิทธิ์ admin ระดับหน่วยงาน |
PMSUserHistoryEntity | tbPMSUserHistory | history ผู้ใช้ |
NotifyEntity | tbNotify | log การแจ้งเตือนที่ส่งไปแล้ว |
PmsAttachFileEntity | pmsAttachFile | ไฟล์แนบ polymorphic |
MDropdownEntity | mDropdown | master lookup กลาง (สถานะ draft/confirm, usageCode ฯลฯ) |
EmailConfigEntity | emailConfig | รายชื่อผู้รับแจ้งเตือนต่อประเภทเหตุการณ์ |
UNVERIFIED — mDropdown ชื่อตารางเดียวกับ benefit-api / recruitment-api แต่ไม่ยืนยันว่า share database เดียวกันหรือมีสำเนาแยกต่อ repo (PMS ใช้ schema pms คนละ DB)
Cross-connection data access
| Connection | Module ที่ inject ใช้จริง | Query pattern |
|---|---|---|
workforce (dbWorkforce) | usagePermission, salaryPercentage, report, image, evaluations, evaluateUserAmount, employeeWorkforce, dropdown, budget, approver (10 module) | TypeORM query builder เป็นหลัก + stored procedure บางจุด |
workforceTemp (dbWorkforce_temp) | rcc เท่านั้น | parameterized EntityManager.query() กับตาราง em* |
vtrc | ไม่พบ inject | — |
main | ทุก feature module | entity + query builder + EXEC sp_* |
หลักฐาน inject workforce — grep @InjectEntityManager('workforce') พบใน:
usagePermission.service.ts/.repositories.tssalaryPercentage.repositories.tsreport.repositories.tsimage.repositories.tsevaluations.repositories.tsevaluateUserAmount.repositories.tsemployee.repositories.ts(employeeWorkforce)dropdown.repositories.tsbudget.repositories.tsapprover.repositories.ts
ข้อสังเกตสำคัญ — pms-api เป็น repo เดียวในกลุ่ม Domain Microservices ที่ query dbWorkforce โดยตรงจาก 10 module พร้อมกัน แทนเรียกผ่าน HTTP (workforce-api) แบบที่บาง flow ของ recruitment ทำ — coupling ลึกและกระจาย ถ้า schema dbWorkforce เปลี่ยนจะกระทบหลายจุดพร้อมกัน
ตาราง workforce ที่พบการอ้างอิงโดยตรง (ไม่ใช่ entity):
| ตาราง / object | ใช้ที่ | ทำอะไร |
|---|---|---|
tbEmployee | usagePermission.repositories.ts | อ่านข้อมูลพนักงาน |
tbEmployeeTriggerQueue | getEmployeeQueue() ที่บรรทัด usagePermission.repositories.ts:507-516 | queue sync promote/transfer/resign เข้า PMS |
emOrgUnitType, emOrgUnit, emPosition, … | rcc.repository.ts บน workforceTemp | master องค์กร RCC |
Raw SQL และ stored procedures
ต่างจาก benefit-api ที่พบ string-interpolated SQL injection หลายจุด — pms-api ส่วนใหญ่ใช้ parameterized query
Stored procedures บน main (parameterized — ปลอดภัยกว่า)
pms-api/src/modules/budget/budget.repositories.ts (branch: uat):260-263
const result = await this.entityManager.query(
'EXEC sp_get_employeeSalary_orgUnit @orgUnitCode = @0, @fiscalYear = @1',
[orgUnitJson, query.fiscalYear],
);pms-api/src/modules/permission/permission.repositories.ts (branch: uat):63-65
buCode
? this.entityManager.query(
'EXEC dbo.sp_GetSalaryPermission @fiscalYear = @0, @buCode = @1, @empCode = @2',
[String(currentFiscalYear), buCode, empCode],
)
: Promise.resolve([]),ทั้งสองจุดใช้ @0, @1 placeholders — ไม่ใช่ string interpolation ของค่าจาก HTTP โดยตรง
Raw SQL บน workforceTemp (parameterized IN-list)
pms-api/src/modules/rcc/rcc.repository.ts (branch: uat):75-86
private async runQuery(baseQuery: string, codes: string[], column: string): Promise<any[]> {
const trimmedCodes = codes.map((code) => code.trim()).filter((code) => code.length > 0);
if (!trimmedCodes.length) {
const query = `${baseQuery} where IsDeleted = 0 AND IsInactive = 0 AND SourceDB = 'dbHRMI_RC_C'`;
return this.workforceTempEntityManager.query(query);
}
const placeholders = trimmedCodes.map((_, idx) => `@${idx}`).join(', ');
const query = `${baseQuery} WHERE ${column} IN (${placeholders}) AND IsDeleted = 0 AND IsInactive = 0 AND SourceDB = 'dbHRMI_RC_C'`;
return this.workforceTempEntityManager.query(query, trimmedCodes);
}ชื่อคอลัมน์ (column) มาจาก constant ใน method ภายใน (OrgUnitTypeCode, PositionCode ฯลฯ) ไม่ใช่จาก user input — ปลอดภัยในทางปฏิบัติ แต่ pattern รวม string ยังควรระวังถ้ามีคนเพิ่ม method ใหม่ที่ส่ง column จากภายนอก
สิ่งที่ไม่พบในรอบนี้
- ไม่พบ pattern
.query(`...${userInput}...`)แบบ SQL injection คลาสสิกเหมือนbenefit-api - ไม่พบ
EntityManager.queryที่ interpolate empCode/orgUnit จาก request โดยไม่มี parameter binding บน connectionmain/workforce
Soft-delete และ cascade
- soft-delete เป็นมาตรฐาน —
isDeletedจากBaseEntity - cascade
trueบน OneToMany ของ history/detail ในหลาย entity (period, indicator, approver, budget, salaryAdjust) — TypeORM จะ cascade save/remove ตาม relation แต่โค้ดส่วนใหญ่ทำ soft-delete ด้วยมือผ่านupdate().set({ isDeleted: true })แทนremove() - ตัวอย่าง soft-delete แบบฟอร์ม —
createForm.service.tsลบ form + ref tables (EvaluateFormHistory,EvaluateFormDt,EvaluateFormKpiRating,EvaluateFormLearning) ใน loop
ตัวอย่าง soft-delete pattern ที่พบซ้ำ
รูปแบบที่ซ้ำในหลาย service (period, salaryRange, indicator, grading, budget ฯลฯ):
await this.entityManager
.createQueryBuilder(SomeEntity, 'hd')
.update()
.set({
isDeleted: true,
updatedBy: req?.user?.profile?.empCode,
updatedByName: req?.user?.profile?.fullName,
updatedAt: new Date(),
})
.where('id = :id', { id: param.id })
.andWhere('isDeleted = 0')
.execute();จากนั้น loop soft-delete ตารางลูก (history / dt) ด้วย FK เดียวกัน — ไม่ใช้ TypeORM cascade: ['soft-remove'] ของ entity decorator โดยตรง
ความสัมพันธ์ข้ามกลุ่ม (ER ระดับสรุป)
tbPeriod ──1:N──► tbPeriodExtend / tbPeriodHistory
│
└── (fiscalYear) ถูกอ้างโดย evaluateUserAmount, approver, budget, salaryAdjust*
tbIndicatorType ──1:N──► tbIndicatorHd ──1:N──► tbIndicatorDt
│
└── indicatorWeightHdId → tbIndicatorWeightHd
tbEvaluateUserAmount ──1:N──► tbEvaluateUser ──1:1──► tbEvaluateForm
├── tbEvaluateFormDt → indicator
├── tbEvaluateFormSummary
├── tbEvaluateFormCourse / Learning
└── *History
tbBudgetHd ──1:N──► tbBudgetDT
tbSalaryAdjustHd ──1:N──► tbSalaryAdjustBu ──► Div ──► Dt
tbApproverHd ──1:N──► tbApproverDt / tbApproverHistory
tbPMSUser ──1:N──► tbPMSUserDepAdmin / tbPMSUserHistoryเส้นประสำคัญ — fiscalYear เป็น natural key ที่เชื่อม period กับเอกสารประเมิน/งบ/ปรับเงินเดือน มากกว่า FK ตรงไป tbPeriod.id ในทุกตาราง (บางตารางเก็บแค่ปีเป็น varchar)
PII และ sensitive columns ที่ denormalize
| ตาราง | คอลัมน์ | เหตุผลที่มี |
|---|---|---|
tbEvaluateForm | identityCard, ชื่อ-นามสกุล, empCode, org names | snapshot ตอนสร้างแบบประเมิน — ไม่พึ่ง workforce ตอนอ่านย้อนหลัง |
tbEvaluateUser | identityCard, ชื่อ, email, org | รายชื่อผู้ถูกประเมินต่อรอบ |
tbPMSUser | โปรไฟล์ + pmsPermission | ผู้ใช้ระบบ |
tbSalaryAdjustDt | จำนวนเงินปรับ / เงินเดือนก่อน-หลัง (field ตาม entity) | เอกสารปรับเงินเดือนรายคน |
authData | token, refreshToken, otp | session store |
เมื่อเขียน report/export ใหม่ อย่า SELECT * จาก evaluate form โดยไม่จำเป็น — และอย่า console.log entity ทั้งก้อน (ดู OBS-PMS-02)
สคริปต์ SQL manual
โฟลเดอร์ pms-api/sql/ (branch: uat) มี:
| กลุ่ม | ตัวอย่างไฟล์ | ใช้ทำอะไร (จากชื่อ) |
|---|---|---|
| Phase scripts | PMS02.sql … PMS13-15.sql, SummarySQLScript_PHASE1.sql, SummarySQLScript_PHASE2_*.sql | สร้าง/แก้ schema ตาม phase |
| Hotfix | hotfix/ | แก้เฉพาะจุดหลัง deploy |
| Report | report/ | สคริปต์รายงาน |
| Seed / data | insert_evaluate_user.sql, insert_evaluate_user_amount.sql, create menu.sql | ใส่ข้อมูลเริ่มต้น |
| Post-UAT | p2-after-uat/ | งานหลัง UAT |
UNVERIFIED — ไม่มี TypeORM migration folder (src/migrations/) ใน repo; ขั้นตอนที่ CI/CD รันสคริปต์เหล่านี้ยังไม่ยืนยันจากโค้ด
นับ entity ยืนยันอีกครั้ง (checklist)
| กลุ่ม | จำนวนไฟล์โดยประมาณ | ตัวอย่าง prefix ตาราง |
|---|---|---|
| Period + Notification | 5 | tbPeriod*, tbConfigNotification* |
| Indicator + Rating + Grading + GradeLimit | 16 | tbIndicator*, tbRatingScale*, tbGrading*, tbGradeLimit* |
| Evaluate (user/form/summary) | 14 | tbEvaluate* |
| Approver + Confirm histories | 5 | tbApprover*, tbConfirmQuotaHistory, tbOrgUnitConfirmHistory |
| Salary + Budget | 18 | tbSalary*, tbPercentSalary*, tbBudget* |
| User / Auth / Master / Notify / Attach | 8 | tbPMSUser*, authData, mDropdown, emailConfig, tbNotify, pmsAttachFile |
| รวม | 66 | — |
ถ้าเพิ่ม entity ใหม่ ให้คง pattern: ตารางหลัก + *History + extend BaseEntity + soft-delete ด้วย isDeleted
ข้อควรจำตอนทำงานกับ persistence
- Trace คะแนน → เงินเดือน:
evaluateFormSummary→evaluateSummaryResult→percentSalaryEmpLevelDt→salaryAdjustDt - ทุกตารางที่มี
*History— query history ก่อนเมื่อ debug ว่าทำไมข้อมูลปัจจุบันเป็นแบบนี้ - ข้อมูลพนักงานสดมาจาก
workforceไม่ใช่ copy ในtbPMSUserเสมอ — sync ผ่าน cron ทุก 1 นาที (ดู 05 Cron jobs) identityCardถูก denormalize ลงtbEvaluateForm/tbEvaluateUser— เป็น PII ที่ต้องระวังตอน export/log- Connection
vtrcอย่าพยายาม inject โดยคิดว่ามี entity — ยังไม่มีผู้ใช้ในโค้ดปัจจุบัน synchronizeเปิดเฉพาะdev— อย่าตั้งNODE_ENV=devชี้ไป UAT/prod DB- Stored procedure ใหม่ควรใช้
@0/@1placeholders เหมือนbudget/permission— ห้ามต่อ string จาก query string - ตารางบน workforce ไม่มี TypeORM entity ใน repo นี้ — schema drift ตรวจจาก SQL error ตอน runtime เป็นหลัก