Skip to content

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

typescript
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

Connectionenv prefixdatabase (UAT)synchronizeautoLoadEntitiesสถานะ
mainDB_*pmsNODE_ENV === 'dev' เท่านั้นเหมือน synchronizeใช้งานจริง — entity ทั้ง 66 ตัว
workforceWORKFORCE_DB_*dbWorkforcefalse เสมอfalseใช้งานจริง — 10 module inject
workforceTempWORKFORCE_TEMP_DB_*dbWorkforce_tempfalse เสมอfalseใช้งานจริง — เฉพาะ module rcc
vtrcVTRC_DB_*vtrcfalse เสมอfalseประกาศไว้แต่ไม่พบ inject

synchronize / autoLoadEntities ของ main เปิดเฉพาะ NODE_ENV === 'dev' ที่ pms-api/config/configuration.ts (branch: uat):15-16 — ใน UAT/prod ต้อง migrate schema ด้วยมือผ่านโฟลเดอร์ pms-api/sql/ (มีสคริปต์ PMS02PMS15, 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

typescript
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

ข้อยกเว้นชื่อตารางที่ไม่ขึ้นต้น tbauthData, mDropdown, emailConfig, pmsAttachFile (ไม่มี prefix tb)

รูปแบบออกแบบหลัก — *History คู่กับตารางหลัก

แทบทุกตารางหลักมีตาราง *History คู่กัน (เช่น tbEvaluateFormtbEvaluateFormHistory, tbSalaryAdjustHdtbSalaryAdjustHdHistory, tbBudgetHdtbBudgetHdHistory, tbPeriodtbPeriodHistory, tbIndicatorHdtbIndicatorHdHistory) — เป็น 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 สำคัญความสัมพันธ์
PeriodEntitytbPeriodfiscalYear, status (draft/confirm), isInactive, ช่วงวันที่ 5 คู่ (settingGoal / checkData / updateResult / evaluation / feedback) + settingGoalSubmit, maximumStartdate1:N → PeriodExtendEntity, PeriodHistoryEntity
PeriodExtendEntitytbPeriodExtendsettingGoalStartExtend/End, buId/buCode/buName, extendRemarkN:1 → PeriodEntity ผ่าน PMSPeriodId
PeriodHistoryEntitytbPeriodHistoryaudit ของการแก้/ขยาย periodN:1 → PeriodEntity
ConfigNotificationEntitytbConfigNotificationnotiTypeCode, isEmail/isVTRC/isLine, target (isEvaluatee/isEvaluator/isApprover/isOrgUnitAdmin), isAdvanceNoti, advanceNotiDay1:N → ConfigNotificationHistoryEntity
ConfigNotificationHistoryEntitytbConfigNotificationHistoryประวัติแก้ 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)บทบาท
IndicatorTypeEntitytbIndicatorTypeประเภทตัวชี้วัดกลาง
IndicatorTypeHistoryEntitytbIndicatorTypeHistoryhistory
IndicatorHdEntitytbIndicatorHdheader ตัวชี้วัด — yearStart/yearEnd, status, usageStatus, refReviseId, FK indicatorWeightHdId
IndicatorHdHistoryEntitytbIndicatorHdHistoryhistory
IndicatorDtEntitytbIndicatorDtรายละเอียดย่อยของตัวชี้วัด
IndicatorWeightHdEntitytbIndicatorWeightHdสัดส่วนน้ำหนัก header
IndicatorWeightDtEntitytbIndicatorWeightDtรายละเอียดน้ำหนัก
IndicatorWeightHistoryEntitytbIndicatorWeightHistoryhistory
RatingScaleEntitytbRatingScalescale การให้คะแนน (1–5, 1–4, ฯลฯ)
RatingScaleHistoryEntitytbRatingScaleHistoryhistory
GradingEntitytbGradingHdนิยามเกรดผลงาน (A/B/C/…)
GradingDtEntitytbGradingDtรายละเอียดเกรด
GradingHistoryEntitytbGradingHdHistoryhistory
GradeLimitHdEntitytbGradeLimitHdขีดจำกัดจำนวน/สัดส่วนคนต่อเกรด (forced distribution)
GradeLimitDtEntitytbGradeLimitDtรายละเอียด limit
GradeLimitHistoryEntitytbGradeLimitHistoryhistory

ความสัมพันธ์หลักของ indicator — pms-api/src/entities/indicatorHd.entity.ts (branch: uat):9-61

typescript
@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)บทบาทความสัมพันธ์
EvaluateUserAmountEntitytbEvaluateUserAmountโควต้า/จำนวนผู้ถูกประเมินต่อหน่วยงานต่อปี1:N → users, history
EvaluateUserAmountHistoryEntitytbEvaluateUserAmountHistoryhistory โควต้าN:1
EvaluateUserEntitytbEvaluateUserรายชื่อผู้ถูกประเมินต่อรอบ — มี empCode, identityCard, org fields, evaluator/agentN:1 → amount; 1:1 → form
EvaluateUserHistoryEntitytbEvaluateUserHistoryhistoryN:1
EvaluateFormEntitytbEvaluateFormแบบประเมิน 1 ใบต่อ 1 คนต่อ 1 รอบ — มี PII (identityCard), คะแนน, สถานะ workflow1:N → Dt, Course, Learning, History, Summary, KpiRating, NoQuotaHistory
EvaluateFormDtEntitytbEvaluateFormDtคะแนนรายตัวชี้วัดN:1 → form; ผูก indicator
EvaluateFormDtHistoryEntitytbEvaluateFormDtHistoryhistory คะแนนรายข้อN:1
EvaluateFormHistoryEntitytbEvaluateFormHistoryhistory สถานะแบบฟอร์มN:1
EvaluateFormKpiRatingEntitytbEvaluateFormKpiRatingrating ราย KPIN:1 → Dt
EvaluateFormCourseEntitytbEvaluateFormCourseคอร์สแนะนำหลังประเมิน (จาก AI)N:1 → form
EvaluateFormLearningEntitytbEvaluateFormLearningแผนพัฒนารายบุคคลN:1 → form
EvaluateFormSummaryEntitytbEvaluateFormSummaryสรุปคะแนนรวมN:1 → form
EvaluateSummaryResultEntitytbEvaluateSummaryResultผลลัพธ์เกรดสุดท้ายผูกสรุปผล
EvaluateFormNoQuotaHistoryEntitytbEvaluateFormNoQuotaHistoryประวัติกรณีไม่มีโควต้าN:1

EvaluateFormEntity มี field PII ชัดเจนที่ pms-api/src/entities/evaluateForm.entity.ts (branch: uat):77-78identityCard ความยาว 17 ตัวอักษร (รูปแบบบัตรประชาชนไทย)

สายข้อมูลคะแนน → เงินเดือนที่ควรจำ:

evaluateFormSummary → evaluateSummaryResult (เกรด)
  → percentSalaryEmpLevelDt (เปอร์เซ็นต์ตามเกรด × ระดับ)
  → salaryAdjustDt (จำนวนเงินจริง)

กลุ่ม Approver + Confirm

Entity class@Entity(name)บทบาท
ApproverHdEntitytbApproverHdสายอนุมัติหลักต่อปี — fiscalYear, status, isInactive
ApproverDtEntitytbApproverDtลำดับผู้อนุมัติรายคน
ApproverHistoryEntitytbApproverHistoryประวัติเปลี่ยนผู้อนุมัติ (delegate)
ConfirmQuotaHistoryEntitytbConfirmQuotaHistoryประวัติยืนยัน/ปฏิเสธโควต้าระดับหน่วยงาน
OrgUnitConfirmHistoryEntitytbOrgUnitConfirmHistoryประวัติยืนยันผลระดับหน่วยงาน

ApproverHdEntity ที่ pms-api/src/entities/approver.entity.ts (branch: uat):6-34 — cascade ลง approverDt และ approverHistory

กลุ่ม Salary (ใหญ่สุดใน repo)

Entity class@Entity(name)บทบาท
SalaryRangeHdEntitytbSalaryRangeHdช่วงเงินเดือนตาม pay grade/position
SalaryRangeDtEntitytbSalaryRangeDTรายละเอียดช่วง
SalaryRangeHistoryEntitytbSalaryRangeHistoryhistory
PercentSalaryHDEntitytbPercentSalaryHDเปอร์เซ็นต์ปรับเงินเดือนตามเกรดผลประเมิน
PercentSalaryEmpLevelEntitytbPercentSalaryEmpLevelระดับพนักงาน
PercentSalaryEmpLevelDtEntitytbPercentSalaryEmpLevelDTเปอร์เซ็นต์ × ระดับ × เกรด
PercentSalaryHistoryEntitytbPercentSalaryHDHistoryhistory
SalaryAdjustHdEntitytbSalaryAdjustHdเอกสารปรับเงินเดือนหลัก — มี sendHrmAt, isHrmiGetData, budget summary
SalaryAdjustHdHistoryEntitytbSalaryAdjustHdHistoryhistory header
SalaryAdjustBuEntitytbSalaryAdjustBuระดับ business unit
SalaryAdjustBuHistoryEntitytbSalaryAdjustBuHistoryhistory BU
SalaryAdjustDivEntitytbSalaryAdjustDivระดับ division
SalaryAdjustDivHistoryEntitytbSalaryAdjustDivHistoryhistory Div
SalaryAdjustDtEntitytbSalaryAdjustDtรายละเอียดต่อคน (จำนวนเงินจริง)
SalaryAdjustDtHistoryEntitytbSalaryAdjustDtHistoryhistory Dt
BudgetHdEntitytbBudgetHdงบประมาณปรับเงินเดือนที่ตั้งไว้
BudgetDtEntitytbBudgetDTรายละเอียดงบต่อหน่วยงาน + flag isAdjust / admin org unit
BudgetHistoryEntitytbBudgetHdHistoryhistory

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-45summaryBudgetStaff / summaryBudgetEmployee เป็นกรอบวงเงินรวม

กลุ่ม User / Auth / Master / Attach / Notify

Entity class@Entity(name)บทบาท
AuthDataEntityauthDatatoken store ของ user login (access + refresh) — pattern เดียวกับ recruitment-api
PMSUserEntitytbPMSUserผู้ใช้งานระบบ PMS + pmsPermission
PMSUserDepAdminEntitytbPMSUserDepAdminสิทธิ์ admin ระดับหน่วยงาน
PMSUserHistoryEntitytbPMSUserHistoryhistory ผู้ใช้
NotifyEntitytbNotifylog การแจ้งเตือนที่ส่งไปแล้ว
PmsAttachFileEntitypmsAttachFileไฟล์แนบ polymorphic
MDropdownEntitymDropdownmaster lookup กลาง (สถานะ draft/confirm, usageCode ฯลฯ)
EmailConfigEntityemailConfigรายชื่อผู้รับแจ้งเตือนต่อประเภทเหตุการณ์

UNVERIFIEDmDropdown ชื่อตารางเดียวกับ benefit-api / recruitment-api แต่ไม่ยืนยันว่า share database เดียวกันหรือมีสำเนาแยกต่อ repo (PMS ใช้ schema pms คนละ DB)

Cross-connection data access

ConnectionModule ที่ 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 moduleentity + query builder + EXEC sp_*

หลักฐาน inject workforce — grep @InjectEntityManager('workforce') พบใน:

  • usagePermission.service.ts / .repositories.ts
  • salaryPercentage.repositories.ts
  • report.repositories.ts
  • image.repositories.ts
  • evaluations.repositories.ts
  • evaluateUserAmount.repositories.ts
  • employee.repositories.ts (employeeWorkforce)
  • dropdown.repositories.ts
  • budget.repositories.ts
  • approver.repositories.ts

ข้อสังเกตสำคัญpms-api เป็น repo เดียวในกลุ่ม Domain Microservices ที่ query dbWorkforce โดยตรงจาก 10 module พร้อมกัน แทนเรียกผ่าน HTTP (workforce-api) แบบที่บาง flow ของ recruitment ทำ — coupling ลึกและกระจาย ถ้า schema dbWorkforce เปลี่ยนจะกระทบหลายจุดพร้อมกัน

ตาราง workforce ที่พบการอ้างอิงโดยตรง (ไม่ใช่ entity):

ตาราง / objectใช้ที่ทำอะไร
tbEmployeeusagePermission.repositories.tsอ่านข้อมูลพนักงาน
tbEmployeeTriggerQueuegetEmployeeQueue() ที่บรรทัด usagePermission.repositories.ts:507-516queue sync promote/transfer/resign เข้า PMS
emOrgUnitType, emOrgUnit, emPosition, …rcc.repository.ts บน workforceTempmaster องค์กร 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

typescript
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

typescript
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

typescript
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 บน connection main/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 ฯลฯ):

typescript
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

ตารางคอลัมน์เหตุผลที่มี
tbEvaluateFormidentityCard, ชื่อ-นามสกุล, empCode, org namessnapshot ตอนสร้างแบบประเมิน — ไม่พึ่ง workforce ตอนอ่านย้อนหลัง
tbEvaluateUseridentityCard, ชื่อ, email, orgรายชื่อผู้ถูกประเมินต่อรอบ
tbPMSUserโปรไฟล์ + pmsPermissionผู้ใช้ระบบ
tbSalaryAdjustDtจำนวนเงินปรับ / เงินเดือนก่อน-หลัง (field ตาม entity)เอกสารปรับเงินเดือนรายคน
authDatatoken, refreshToken, otpsession store

เมื่อเขียน report/export ใหม่ อย่า SELECT * จาก evaluate form โดยไม่จำเป็น — และอย่า console.log entity ทั้งก้อน (ดู OBS-PMS-02)

สคริปต์ SQL manual

โฟลเดอร์ pms-api/sql/ (branch: uat) มี:

กลุ่มตัวอย่างไฟล์ใช้ทำอะไร (จากชื่อ)
Phase scriptsPMS02.sqlPMS13-15.sql, SummarySQLScript_PHASE1.sql, SummarySQLScript_PHASE2_*.sqlสร้าง/แก้ schema ตาม phase
Hotfixhotfix/แก้เฉพาะจุดหลัง deploy
Reportreport/สคริปต์รายงาน
Seed / datainsert_evaluate_user.sql, insert_evaluate_user_amount.sql, create menu.sqlใส่ข้อมูลเริ่มต้น
Post-UATp2-after-uat/งานหลัง UAT

UNVERIFIED — ไม่มี TypeORM migration folder (src/migrations/) ใน repo; ขั้นตอนที่ CI/CD รันสคริปต์เหล่านี้ยังไม่ยืนยันจากโค้ด

นับ entity ยืนยันอีกครั้ง (checklist)

กลุ่มจำนวนไฟล์โดยประมาณตัวอย่าง prefix ตาราง
Period + Notification5tbPeriod*, tbConfigNotification*
Indicator + Rating + Grading + GradeLimit16tbIndicator*, tbRatingScale*, tbGrading*, tbGradeLimit*
Evaluate (user/form/summary)14tbEvaluate*
Approver + Confirm histories5tbApprover*, tbConfirmQuotaHistory, tbOrgUnitConfirmHistory
Salary + Budget18tbSalary*, tbPercentSalary*, tbBudget*
User / Auth / Master / Notify / Attach8tbPMSUser*, authData, mDropdown, emailConfig, tbNotify, pmsAttachFile
รวม66

ถ้าเพิ่ม entity ใหม่ ให้คง pattern: ตารางหลัก + *History + extend BaseEntity + soft-delete ด้วย isDeleted

ข้อควรจำตอนทำงานกับ persistence

  1. Trace คะแนน → เงินเดือน: evaluateFormSummaryevaluateSummaryResultpercentSalaryEmpLevelDtsalaryAdjustDt
  2. ทุกตารางที่มี *History — query history ก่อนเมื่อ debug ว่าทำไมข้อมูลปัจจุบันเป็นแบบนี้
  3. ข้อมูลพนักงานสดมาจาก workforce ไม่ใช่ copy ใน tbPMSUser เสมอ — sync ผ่าน cron ทุก 1 นาที (ดู 05 Cron jobs)
  4. identityCard ถูก denormalize ลง tbEvaluateForm / tbEvaluateUser — เป็น PII ที่ต้องระวังตอน export/log
  5. Connection vtrc อย่าพยายาม inject โดยคิดว่ามี entity — ยังไม่มีผู้ใช้ในโค้ดปัจจุบัน
  6. synchronize เปิดเฉพาะ dev — อย่าตั้ง NODE_ENV=dev ชี้ไป UAT/prod DB
  7. Stored procedure ใหม่ควรใช้ @0/@1 placeholders เหมือน budget/permission — ห้ามต่อ string จาก query string
  8. ตารางบน workforce ไม่มี TypeORM entity ใน repo นี้ — schema drift ตรวจจาก SQL error ตอน runtime เป็นหลัก

ไป บท 4 Auth + integrations