Skip to content

3.6 · Controller layer survey

บทนี้เล่าว่ามี controller อะไรบ้าง แต่ละตัวทำอะไร และจัดเป็น 7 cluster เพื่อให้หาเร็วขึ้น

ถ้าเจอ "function นี้อยู่ที่ไหน" — เริ่มที่บทนี้


ภาพรวม

api/src/lib/controllers/
├── 30 โฟลเดอร์
├── 66 ไฟล์ JS
└── ~25,800 บรรทัดรวม

ทุก controller ทำหน้าที่เดียวกัน — orchestration layer ที่

  1. รับ input จาก resolver (typeDefs)
  2. เรียก repository (lib/repositories) หรือ centralize/ (bridge)
  3. รวมผลจากหลาย source
  4. return กลับไป resolver

controller ไม่ควรทำ DB access ตรง — แต่ใน VTRC มีบางที่ทำ (layer inversion ดู บท 3.5)


Layering ที่ควรจะเป็น

typeDefs  ─►  resolvers  ─►  controllers  ─►  repositories  ─►  Models

                                                    └─► centralize/ (bridge to cu-central-api)

ในทางปฏิบัติ — controller บางตัวเรียก repository + centralize + Models พร้อมกัน และบาง repository เรียก controller (layer inversion)


7 cluster ของ controllers

ClusterControllersLOCTheme
A · Auth & Sessionauth/, user/, role/, verification/, version/, consent/, policy/~960login, JWT, RBAC, OTP
B · HR Master Bridgeprofile/, masterData/ (8 files), organization/, timeAttendance/~3,700read-heavy aggregator จาก centralize
C · Leave (HRMI)hrmi/~2,600leave document lifecycle
D · Welfare Withdrawwithdraw/ (3 files), slip/~5,200hospital reimbursement + payroll slip
E · Lifecycle (Fund/Study/Conicle)fund/, study/ (11 files), childTuition/, conicle/~5,100provident fund, study leave, LMS
F · Content & Commspost/, document/, course/, career/, contactUs/, notification/, createNotification/, sendEmail/~1,700news, docs, courses, push notification, email
G · Reports & FiletaxDraft/, statLog/, excel/, file/, redis.controller.js~2,700tax form, stats cron, Excel hub, file storage

Cluster A · Auth & Session (~960 LOC)

auth/ (402 LOC)

FileLOCTop exports
auth/auth.js333login, loginBackoffice, loginBypass, getPayLoad, renewRefreshToken, signOutSystem, captcha, verifyEmpCodeLogin
auth/authSessionMoblie.js69findAuthSessionMoblie, createAccessAuthSessionMoblie (mobile public-key session)

ดู บท 3.3 สำหรับรายละเอียด authentication internals

จุดสังเกต

  • authSessionMoblie.js — typo "Moblie" (ผิดจาก Mobile) เป็นชื่อไฟล์ที่ import หลายที่
  • auth.js:20 — commented-out logTxtActive (transaction activity log disabled)

user/ (372 LOC)

FileLOCTop exports
user/user.js330getUserById, createUser, changePassword, changeRole, forgetPassword, resetPassword, findEmpDetailByEmpCode
user/userDivision.js42getCurrentUserDivision, createUserDivision, destroyUserDivision

จุดสังเกต — forgetPassword flow ใช้ client-supplied birthDate + identityCard ผ่าน centralize checker → ต้อง verify ฝั่ง centralize ว่ามี rate limit

role/ (180 LOC)

FileLOCTop exports
role/role.js102fetchRoleBackoffice, createRole, updateRole, destroyRole
role/roleAccess.js78listRoleAccess, createRoleAccess

จุดสังเกต — fail-open

javascript
// role/roleAccess.js (simplified)
catch (err) {
  return MENUS_BACK_OFFICE  // ← return all menus ตอน error
}

ถ้า query มี error → return ทุก menu ทำให้ user ผ่านได้ทุกอย่าง

FileLOCNotes
verification/verification.js38100% pass-through ไป repository
version/version.js51app version check — เปรียบเทียบ semver
consent/consent.js19checkConsent return { data: null, isLastVerson: false }stub ไม่ทำงานจริง
policy/policy.js20มีแค่ commented mock block

Cluster B · HR Master Bridge (~3,700 LOC)

cluster นี้ read-heavy — ส่วนใหญ่เป็น aggregator ที่ดึงข้อมูล HR จาก centralize แล้ว return กลับ

profile/ (321 LOC)

export ทั้งหมดเป็น 1-line pass-through ไป repositories/profile/profile.js

Exportใช้ตอนไหน
fetchProfileprofile หลักของพนักงาน
fetchEmployeeProfiledetail profile
fetchEmployeeBankAccountbook bank
fetchEmployeeProvidentFundกองทุน
fetchEmployeeTaxภาษี
fetchEmployeeFamilyครอบครัว
fetchEmployeeCompensationค่าตอบแทน

masterData/ (8 files, 2,538 LOC)

FileLOCTheme
referenceData.js780generic reference data CRUD + domain fetchers
bank.js276bank master + deactivation guard
bankAccount.js259hospital bank account
masterDropdown.js221dropdown source + raw SQL (2 queries)
masterConfig.js206provident fund window config
approver.js137master approver per orgCode
bankConfig.js78bank export config
orgSystem.js41org system (ค่าคงที่องค์กร)

จุดสังเกต — bank.js:43-72 มี referential integrity check ที่ block deactivate ของ bank ที่มี WDHospital อยู่ใน status pending

organization/ (386 LOC)

FileLOCNotes
organization.js245CRUD + backoffice list
organizationGroup.js96org group
organizationDivision.js45division scoping

timeAttendance/ (9 LOC — stub)

javascript
// timeAttendance/timeAttendance.js (full content essentially)
import { ... }
export const fetchTimeAttendance = async (...) => {
  return await repositories.timeAttendance(...)
}

มีแค่ pass-through แต่ import bcryptjs, jsonwebtoken, JWT_SECRETunused dead imports


Cluster C · Leave HRMI (~2,600 LOC)

cluster นี้มี controller เดียว แต่ใหญ่ที่สุดเป็นอันดับ 2

hrmi/hrmi.js (2,600 LOC, 40+ exports)

Export groupตัวอย่าง
MedicalgetMedical, hrmiGetMedicalCD
ApprovergetApprover, getLeaveApprover
Leave typegetLeaveType, getLeaveDayAll
Leave documentcreateLeaveDocument, editLeaveDocument, deleteLeaveDocument, getLeaveDocumentList, getLeaveDocumentDetail
AttachmenthrmiCreateAttachmentFileCD, hrmiUpdateAttachmentFileCD
Working daygetWorkingDay
HelpergetTokenCDByEmpCode (ใช้ใน cluster D ด้วย)

Business rule anchor

1295:1319:vtrc-api/api/src/lib/controllers/hrmi/hrmi.js
// leave-day validation
if (maxLeaveDay && args.leaveDay > maxLeaveDay) → reject
if (minLeaveDay && args.leaveDay < minLeaveDay) → reject
// admin bypass
if (adminDetail.orgUnitCode.substring(0,4) !== '0734' && !args.adminCanPass) {
  // apply rules
}

รายละเอียด business rule เต็มอยู่ใน Volume 12 Business Logic

จุดสังเกต

  • 5 raw SQL QueryTypes.SELECT (:493, 583, 1061, 2126, 2447, 2487, 2560)
  • console.log debug จำนวนมาก (เช่น :1286, 1289, 1330, 1337)
  • import googleapis/build/src/apis/jobs ที่ :28 ที่ดูเหมือน unused

Cluster D · Welfare Withdraw (~5,200 LOC)

cluster ใหญ่ที่สุด — เป็นหัวใจทางการเงินของระบบ

withdraw/ (3 files, 5,164 LOC)

FileLOCTop exports
hospital.js3,179fetchListWDHospital, createWDHospitalv2, updateWDHospital, createDelegateApprover, importUpdateStatus (~32 exports)
hospital.report.js1,37812 report aggregators (getSummaryWithdrawDivisionData, getListSuccessPaymentData, ...)
hospital.template.js60512 PDF/Excel template renderers

slip/ (335 LOC)

ExportPurpose
fetchSlipดึง payslip ของเดือน/ปี
streamPayRollSilpPDFstream PDF (แท้จริงแล้วสร้าง URL ให้ REST endpoint)
fetchUrlSalaryCertURL สำหรับ salary certificate PDF
howToOpenPayrollSlipThai help text

จุดสังเกต — bug ที่น่าสนใจ

javascript
// slip/slip.js:78 (simplified)
await new Promise(r => setTimeout(r, 3000))  // ← artificial 3s sleep

dropDownSlipPayment มี artificial delay 3 วินาทีโดยไม่มีเหตุผล — คงติดมาจาก debugging แล้วลืม remove

State machine anchor

withdraw/hospital.js:2912-3025 — getActionStatus / validateUpdateStatus
withdraw/hospital.js:3126-3172 — checkMedicalExpenseRight, checkResignationRight

13-state machine เต็มอยู่ใน Volume 12


Cluster E · Lifecycle — Fund/Study/Conicle (~5,100 LOC)

fund/ (1,097 LOC)

FileLOCTop exports
fund.js927fetchEmployeeProvidentFund, createFundRequest, updateDelegatesExpire (cron)
fund.report.js170report aggregator

จุดสังเกต — fire-and-forget

926:928:vtrc-api/api/src/lib/controllers/fund/fund.js
export const updateDelegatesExpire = async () => {
    const result = await Models.db.query('UPDATE Delegates SET isActive = 2 WHERE NOW() > endDate')
    // ไม่ await — fail ก็เงียบ
}

study/ (11 files, 2,894 LOC)

FileLOCTheme
studyLeaveRequest.js1,282ใบลาศึกษาต่อ — main controller
studyEducationFeeV2.js300ค่าเล่าเรียน V2
อีก 9 ไฟล์~310 รวมsub-entity CRUD (detail, strategy, scholarship, ...)

แต่ละ sub-entity มี pattern เดียวกัน — create*, update*, destroyByCondition, findByCondition, destroyByPkId

จุดสังเกต — running number race

javascript
// study/studyEducationFeeV2.js:168 (pattern)
const last = await Models.db.query('SELECT studyEducationDocumentNo FROM ... ORDER BY ... LIMIT 1')
const newNo = `FE${YYMM}-${parseInt(last) + 1}`

ถ้ามี concurrent insert 2 ตัว → ทั้งคู่เห็น last = X → ทั้งคู่ generate X+1 → unique constraint error (ถ้ามี) หรือ row ซ้ำ (ถ้าไม่มี)

childTuition/ (59 LOC)

3 exports ทั้งหมดเป็น pass-through ไป centralize

conicle/ (305 LOC)

ExportPurpose
authConiclelogin เข้า Conicle ด้วย cookie + csrf
(อื่นๆ)sync course catalog จาก Conicle มา local Models.Conicle

env var มี typo — CONINCLE_USERNAME vs CONICLE_PAGE (prefix ไม่เหมือนกัน)


Cluster F · Content & Comms (~1,700 LOC)

post/ (272 LOC)

news + announcement — CRUD ปกติ + highLightPost (cap 3 active highlights per division)

document/ (696 LOC)

document library — fetchListDocumentByConditions, createDocument, highLightDocument

course/, career/, contactUs/ (~580 LOC รวม)

CRUD pattern เดียวกัน — create*, update*, destroy*, highLight* + division scoping

notification/ (779 LOC)

FileLOCTop exports
notification.js716fetchListNotification, sendNotification (fan-out หลาย target)
notificationRequest.js39bulk request
queneNotification.js24queue helper (typo "quene")

จุดสังเกต — function พัง

127:134:vtrc-api/api/src/lib/controllers/notification/notification.js
export const checkRequireNotification = async (...) => {
  // ... 
  if (requireNotification) {  // ← undefined variable
    // ...
  }
}

ถ้า code path นี้ถูกเรียก → ReferenceError

createNotification/ (33 LOC)

dispatch notification สำหรับ MEETING group

จุดสังเกต — broken catch

javascript
// createNotification/createNotification.js (simplified)
try {
  // await Models.db.transaction()  ← commented out
  // ...
} catch (err) {
  await t.rollback()  // ← undefined
  log.error(...)       // ← log ไม่ได้ import
}

ถ้า catch ถูกเรียก → ReferenceError ซ้อน

sendEmail/ (80 LOC)

SMTP email — ใช้ nodemailer + config จาก Master_configs table

จุดสังเกต

  • subject มี append ต่อท้ายเสมอ (:38)
  • error log ใช้ method name ผิด — 'Method:createStudyAttachment' (copy-paste)

Cluster G · Reports & File (~2,700 LOC)

taxDraft/ (1,944 LOC)

ExportPurpose
templateFormTaxform schema ขนาดใหญ่ (~1,500 LOC ของ JSON literal)
fetchTaxDraft, createTaxDraft, updateTaxDraftCRUD
guideLineTax, confirmTextTaxDraft, detailTaxstatic content
dropdownListRelation, validateInputhelpers

~80% ของไฟล์เป็น form template literal — เหมาะกับย้ายไปเป็น i18n หรือ data-driven

statLog/ (243 LOC)

cron ที่รันทุกวันที่ 00:30 — ดึง activation log จาก END_POINT_LOG_EVENT แล้ว bulk INSERT ลง db2

จุดสังเกต — SQL injection risk

javascript
// statLog/statLog.js:36 (pattern)
const empCodeList = empCodes.map(e => `'${e}'`).join(',')
await Models.db.query(`SELECT ... WHERE empCode IN (${empCodeList})`)

ถ้า log service ถูก compromise แล้วส่ง empCode ที่มี quote → SQL injection

excel/ (167 LOC + repository 614 LOC)

Excel export/import hub

ExportPurpose
downloadExcelgeneric per-table download — รับ req.params.table เป็น dynamic
createExportFileFMIS Excel + KPayroll text + SmartCredit text + cash
uploadExcelread + insert

จุดสังเกต — dynamic table

downloadExcel รับ table parameter → ผ่านไป queryDataForExcelExport(table, condition) ใน repository → Models[table].findAll(...) → ถ้า table มาจาก user input = RCE เทียบเท่า

file/ (366 LOC)

FileLOCTop exports
file.js362saveFile, saveFilev2, getFileById, setUpFileBase64FromResolver
zip.js4(debug leftover)

จุดสังเกต — hardcoded dev path

4:4:vtrc-api/api/src/lib/controllers/file/zip.js
const storagePath = "/Users/porch/Desktop/vtrc-api/api/storage"

path ค้างจากเครื่อง developer ตัวเดิม — ถ้าเรียกใช้จะ fail ในทุก environment อื่น

file.js:21 — stray character

javascript
const outStream = createWriteStream(tmpFile, { flags: "w" });
W  // ← stray token, expression statement (harmless but copy-paste)

redis.controller.js (19 LOC, root-level)

javascript
export const getCacheData = async (name, uid) => {
  tr  // ← broken — ReferenceError
  ...
}

function พัง — แต่ไม่มีใครเรียกเพราะ call site ทุกที่ comment อยู่ (ดู บท 3.4)


Layer inversion — ที่ที่ controller ถูกเรียกกลับจาก repository

ปกติ arrow ลง — controller เรียก repository แต่มี 3 กรณีที่กลับด้าน

RepositoryController ที่ถูกเรียกเหตุผล
profile/profile.jswithdraw/hospital, hrmi/hrmi, masterData/approverprofile ต้องรู้ approval chain + leave context
slip/slip.jsmasterData/mapGroupDocumentsalary cert eligibility check
notification/notification.jsmasterData/referenceDatajoin dropdown data

ผล — testing ทั้ง 3 repo ต้อง mock controller ที่อยู่ข้างบน


เคล็ดลับตอนทำงานจริง

ถ้าจะเพิ่ม feature ใหม่ — เริ่มที่ไหน

  1. เขียน typeDefs ใน typeDefs/<domain>.js (หรือไฟล์ใหม่)
  2. เขียน resolver ใน resolvers/<domain>.js
  3. เขียน controller ใน lib/controllers/<domain>/
  4. เขียน repository ใน lib/repositories/<domain>/
  5. ถ้าต้องเรียก centralize → เขียน wrapper ใน centralize/controllers/<domain>.js

ห้ามข้าม layer — ถ้า resolver เรียก Models ตรง หรือ controller เรียก typeDefs จะเละ

ถ้า debug "function นี้ทำงานผิด"

เริ่มที่ typeDefs → resolver → controller → repository → Models/centralize ตามลำดับ อย่ากระโดดไปแก้ controller ก่อนเข้าใจ resolver

await arr.map(async ...) — bug ระบบ-wide

มี pattern ที่ผิดหลายไฟล์

javascript
// ผิด — await ไม่ได้รอ Promise.all
await arr.map(async (item) => {
  await Models.X.create(...)
})

ที่ถูก

javascript
await Promise.all(arr.map(async (item) => {
  await Models.X.create(...)
}))

หรือใช้ for...of ถ้าต้องการ sequential

พบใน document/documentDivision.js:8, career/careerDivision.js:9, course/courseDivision.js, post/postDivision.js, contactUs/*

Controller ที่ยาวเกินไป — แบ่งอย่างไร

withdraw/hospital.js (3,179 LOC) เป็น candidate สำหรับแบ่งเป็น

  • hospital.create.js — create path
  • hospital.update.js — update + state machine
  • hospital.delegate.js — approval delegation
  • hospital.list.js — list/fetch
  • hospital.report.js (มีแล้ว)

แต่การแบ่งกระทบ caller ทุกที่ — ต้อง coordinate import update


ขั้นตอนถัดไป

ไป บท 3.7 REST surface (routes.js)