3.1.6 · Controller layer survey
บทนี้เล่าว่ามี controller อะไรบ้าง แต่ละตัวทำอะไร จัดเป็น 7 cluster เพื่อให้ค้นเร็วขึ้น ถ้าเจอคำถามแบบ "function นี้อยู่ที่ไหน" ให้เริ่มที่บทนี้ บทนี้จะลงลึกถึง pattern ที่พบบ่อยในแต่ละ cluster (raw SQL, fire-and-forget, await arr.map, broken catch) พร้อม code trace และ line citation เพื่อให้ตอบโจทย์ทั้งคนอ่านใหม่และคนมา debug
ภาพรวม (verify กับ filesystem 2026-07-13)
api/src/lib/controllers/
├── 31 โฟลเดอร์ + 1 ไฟล์เดี่ยว (redis.controller.js)
├── 69 ไฟล์ JS รวม
└── 20,698 บรรทัดรวม (ตรงกับ canonical-truths G4)แก้จาก v1 — v1 ระบุ "30 โฟลเดอร์, 66 ไฟล์, ~25,800 บรรทัด" นับใหม่จาก filesystem ได้ 31 โฟลเดอร์, 69 ไฟล์ (รวม
redis.controller.jsที่เป็นไฟล์เดี่ยว), 20,698 บรรทัด — จำนวนโฟลเดอร์ตรงกับ canonical-truths G4 ส่วน LOC ที่ต่างกันมาก (25,800 → 20,698) มาจากการนับผิดของ v1 ไม่ใช่โค้ดหด ยืนยันด้วยfind lib/controllers -name "*.js" -exec wc -l {} +
ทุก controller ทำหน้าที่เป็น orchestration layer ที่ (1) รับ input จาก resolver (2) เรียก repository หรือ centralize/ bridge (3) รวมผลจากหลาย source (4) return กลับไป resolver controller ไม่ควรทำ DB access ตรง แต่ใน VTRC มีบางไฟล์ที่ทำ (layer inversion — ดู 3.1.5)
typeDefs ─► resolvers ─► controllers ─► repositories ─► Models
│
└─► centralize/ (bridge to cu-central-api)ในทางปฏิบัติ controller บางตัวเรียก repository + centralize + Models พร้อมกัน และบาง repository เรียก controller กลับ (layer inversion) เป็นสาเหตุหลักที่ทำให้ unit-test controller ยาก เพราะ mock ต้องครอบคลุมหลายชั้น
7 cluster ของ controllers
| Cluster | Controllers | LOC (verified) | Theme |
|---|---|---|---|
| A · Auth & Session | auth/(402), user/(372), role/(180), verification/(38), version/(51), consent/(19), policy/(20) | ~1,082 | login, JWT, RBAC, OTP |
| B · HR Master Bridge | profile/(321), masterData/(1,998, 8 files), organization/(386), timeAttendance/(9) | ~2,714 | read-heavy aggregator จาก centralize |
| C · Leave (HRMI) | hrmi/(2,600) | 2,600 | leave document lifecycle |
| D · Welfare Withdraw | withdraw/(5,162, 3 files), slip/(335) | ~5,497 | hospital reimbursement + payroll slip |
| E · Lifecycle | fund/(1,097), study/(2,008, 11 files), childTuition/(59), conicle/(305) | ~3,469 | provident fund, study leave, LMS |
| F · Content & Comms | post/(272), document/(696), course/(140), career/(119), contactUs/(326), notification/(779), createNotification/(33), sendEmail/(80) | ~2,445 | news, docs, courses, push, email |
| G · Reports & File | taxDraft/(1,944), statLog/(243), excel/(167), file/(366), redis.controller.js(19, root) | ~2,739 | tax form, stats cron, Excel hub, file storage |
Cluster A · Auth & Session
auth/ (402 LOC, 2 ไฟล์)
auth/auth.js (333 LOC) export หลักคือ login, loginBackoffice, loginBypass, getPayLoad, renewRefreshToken, signOutSystem, captcha, verifyEmpCodeLogin — ดู 3.1.3 สำหรับรายละเอียด authentication internals
auth/authSessionMoblie.js (69 LOC) — เก็บ mobile public-key session (findAuthSessionMoblie, createAccessAuthSessionMoblie) ชื่อไฟล์มี typo "Moblie" (ผิดจาก Mobile) แต่เป็นชื่อที่ import หลายที่ การแก้ typo นี้ต้อง update import ทุก caller พร้อมกัน
vtrc-api/api/src/lib/controllers/auth/auth.js:20 — มี logTxtActive แบบ commented-out ค้างอยู่ คือ transaction activity log ที่ถูก disable ไว้
user/ (372 LOC, 2 ไฟล์)
user/user.js (330 LOC) export หลักคือ getUserById, createUser, changePassword, changeRole, forgetPassword, resetPassword, findEmpDetailByEmpCode, findEmpDetailByEmpCodeArray
จุดสังเกต — forgetPassword flow ใช้ client-supplied birthDate + identityCard ผ่าน centralize checker เพื่อ verify ตัวตนก่อน reset ระบบจึง rely ฝั่ง centralize ว่ามี rate limit ถ้า centralize ไม่ limit → brute force birthDate+identityCard ได้
user/userDivision.js (42 LOC) — getCurrentUserDivision, createUserDivision, destroyUserDivision
role/ (180 LOC, 2 ไฟล์) — fail-open anti-pattern
vtrc-api/api/src/lib/controllers/role/roleAccess.js
// pattern — simplified
catch (err) {
return MENUS_BACK_OFFICE // ← return all menus ตอน error
}ถ้า query มี error (DB down, syntax error, timeout) → catch block return ทุก menu ทำให้ user ที่ login อยู่ตอนนั้นผ่านเข้า menu ได้ทุกอย่าง เป็น security anti-pattern คลาสสิก (fail-open) ที่ควรเป็น fail-closed (return [] หรือ rethrow)
role/role.js (102 LOC) — fetchRoleBackoffice, createRole, updateRole, destroyRole
verification/, version/, consent/, policy/ (128 LOC รวม)
| File | LOC | Notes |
|---|---|---|
verification/verification.js | 38 | 100% pass-through ไป repository |
version/version.js | 51 | app version check — เปรียบเทียบ semver |
consent/consent.js | 19 | checkConsent return { data: null, isLastVerson: false } — stub ไม่ทำงานจริง (เห็น typo isLastVerson ด้วย) |
policy/policy.js | 20 | มีแค่ commented mock block |
cluster นี้เป็น "API surface ที่วางไว้แต่ยังไม่ implement" จริง — มี resolver, typeDefs, controller ครบ แต่ logic ว่าง
Cluster B · HR Master Bridge
cluster นี้ read-heavy — ส่วนใหญ่เป็น aggregator ที่ดึงข้อมูล HR จาก centralize แล้ว return กลับ แทบไม่มี business logic เป็นของตัวเอง
profile/ (321 LOC)
export ทั้งหมดเป็น 1-line pass-through ไป repositories/profile/profile.js
| Export | ใช้ตอนไหน |
|---|---|
fetchProfile | profile หลักของพนักงาน |
fetchEmployeeProfile | detail profile |
fetchEmployeeBankAccount | book bank |
fetchEmployeeProvidentFund | กองทุน |
fetchEmployeeTax | ภาษี |
fetchEmployeeFamily | ครอบครัว |
fetchEmployeeCompensation | ค่าตอบแทน |
ไฟล์นี้เป็นที่ที่ layer inversion เกิด — repository เรียก controller อื่นกลับ (ดูหัวข้อ Layer inversion ด้านล่าง)
masterData/ (8 files, 1,998 LOC)
| File | LOC | Theme |
|---|---|---|
referenceData.js | 780 | generic reference data CRUD + domain fetchers |
bank.js | 276 | bank master + deactivation guard |
bankAccount.js | 259 | hospital bank account |
masterDropdown.js | 221 | dropdown source + raw SQL (2 queries) |
masterConfig.js | 206 | provident fund window config |
approver.js | 137 | master approver per orgCode |
bankConfig.js | 78 | bank export config |
orgSystem.js | 41 | org system constant |
จุดสังเกต — bank.js:43-72 มี referential integrity check ที่ block deactivate ของ bank ที่มี WDHospital อยู่ใน status pending เป็น guard ที่ดี (ป้องกันลบ master ที่มี transaction reference)
organization/ (386 LOC, 3 ไฟล์)
organization.js (245 LOC) — CRUD + backoffice list organizationGroup.js (96 LOC) — org group organizationDivision.js (45 LOC) — division scoping
timeAttendance/ (9 LOC — stub)
// timeAttendance/timeAttendance.js (full content essentially)
import { ... } // bcryptjs, jsonwebtoken, JWT_SECRET
export const fetchTimeAttendance = async (...) => {
return await repositories.timeAttendance(...)
}มีแค่ pass-through แต่ import bcryptjs, jsonwebtoken, JWT_SECRET โดยไม่ได้ใช้ — unused dead imports ค้างจาก copy-paste จาก controller อื่น เป็น load เล็กน้อยตอน module load แต่อันตรายตรงที่ทำให้คนอ่านเข้าใจผิดว่ามี auth logic
Cluster C · Leave HRMI
cluster นี้มี controller เดียว แต่ใหญ่เป็นอันดับ 2 ของระบบ
hrmi/hrmi.js (2,600 LOC, 40+ exports)
| Export group | ตัวอย่าง |
|---|---|
| Medical | getMedical, hrmiGetMedicalCD |
| Approver | getApprover, getLeaveApprover |
| Leave type | getLeaveType, getLeaveDayAll |
| Leave document | createLeaveDocument, editLeaveDocument, deleteLeaveDocument, getLeaveDocumentList, getLeaveDocumentDetail |
| Attachment | hrmiCreateAttachmentFileCD, hrmiUpdateAttachmentFileCD |
| Working day | getWorkingDay |
| Helper | getTokenCDByEmpCode (ใช้ใน cluster D ด้วย — cross-cluster coupling) |
Business rule anchor — leave-day validation
vtrc-api/api/src/lib/controllers/hrmi/hrmi.js:1295-1319
// pattern — simplified
// 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 14 Business Logic
จุดสังเกต
- 5+ raw SQL
QueryTypes.SELECTกระจายอยู่ (:493, 583, 1061, 2126, 2447, 2487, 2560) — bypass repository layer console.logdebug จำนวนมาก (เช่น:1286, 1289, 1330, 1337) — ไม่ใช่ structured loggingvtrc-api/api/src/lib/controllers/hrmi/hrmi.js:28import{ jobs } from 'googleapis/build/src/apis/jobs'ที่ไม่ได้ใช้ในไฟล์ — load module ทั้ง Google API library เปล่าๆ ทำให้ cold start ช้าลง
Cluster D · Welfare Withdraw
cluster ใหญ่ที่สุดในระบบ — เป็นหัวใจทางการเงินของระบบ (hospital reimbursement + payroll slip)
withdraw/ (3 files, 5,162 LOC)
| File | LOC | Top exports |
|---|---|---|
hospital.js | 3,179 | fetchListWDHospital, createWDHospitalv2, updateWDHospital, createDelegateApprover, importUpdateStatus (~32 exports) |
hospital.report.js | 1,378 | 12 report aggregators (getSummaryWithdrawDivisionData, getListSuccessPaymentData, ...) |
hospital.template.js | 605 | 12 PDF/Excel template renderers |
ไฟล์ hospital.js import หนักมาก 30+ modules (ดู header :1-29) รวม controller อื่น (user/user, notification/notification, hrmi/hrmi, excel/excel, file/file) และ centralize bridge (centralize/controllers/user) — เป็น god-controller ที่ coupling สูง
vtrc-api/api/src/lib/controllers/withdraw/hospital.js:31-34
const MEDICAL_NOTI_TYPE = {
detail: "MEDICAL_DETAIL",
approving: "MEDICAL_APPROVING"
}constant นี้ใช้ใน notification dispatch ตอนสร้าง/อนุมัติ WD — เป็น business rule ที่ embed ใน controller แทนที่จะอยู่ใน config
slip/ (335 LOC)
| Export | Purpose |
|---|---|
fetchSlip | ดึง payslip ของเดือน/ปี |
streamPayRollSilpPDF | stream PDF (แท้จริงแล้วสร้าง URL ให้ REST endpoint) |
fetchUrlSalaryCert | URL สำหรับ salary certificate PDF |
howToOpenPayrollSlip | Thai help text อธิบายรหัสผ่าน DDMMYYYY |
dropDownSlipPayment | dropdown ประเภทเอกสาร |
จุดสังเกต — artificial delay
vtrc-api/api/src/lib/controllers/slip/slip.js:78
await new Promise(r => setTimeout(r, 3000)) // ← artificial 3s sleepdropDownSlipPayment มี artificial delay 3 วินาทีโดยไม่มีเหตุผล คงติดมาจาก debugging แล้วลืม remove — ทุกครั้งที่ frontend เปิดหน้า slip dropdown ต้องรอ 3 วินาทีเสมอ
vtrc-api/api/src/lib/controllers/slip/slip.js:307
await new Promise(r => setTimeout(r, 3000)) // ← duplicate ใน dropDownTypeSalaryCertdelay ซ้ำในฟังก์ชัน dropDownTypeSalaryCert ด้วย — เป็น pattern ที่ copy-paste กัน
State machine anchor
withdraw/hospital.js:2912-3025 — getActionStatus / validateUpdateStatus
withdraw/hospital.js:3126-3172 — checkMedicalExpenseRight, checkResignationRight13-state machine เต็มอยู่ใน Volume 14 Business Logic
Cluster E · Lifecycle — Fund/Study/Conicle
cluster ที่ครอบคลุม lifecycle ของพนักงาน (กองทุน, ลาศึกษา, LMS)
fund/ (2 files, 1,097 LOC)
fund.js (927 LOC) export หลักคือ fetchEmployeeProvidentFund, createFundRequest, updateDelegatesExpire (cron — ดู 3.1.8) fund.report.js (170 LOC) — report aggregator
จุดสังเกต — fire-and-forget (CORR-VTRC-API-01 pattern)
vtrc-api/api/src/lib/controllers/fund/fund.js:926-928
export const updateDelegatesExpire = () => {
Models.db.query(`UPDATE vtrc.Delegates SET isActive = :isActive WHERE NOW() > endDate and isActive != 0`, { replacements: { isActive: 2 }, type: QueryTypes.UPDATE })
}function นี้ไม่ async, ไม่ return promise ที่ caller จะ await ได้ — caller ทุกที่ (cron) เรียกแบบ sync ปล่อยให้ query ทำงานเอง ถ้า query fail → unhandledRejection ที่ไม่มีใครเห็น ไม่มี log ไม่มี retry เป็น pattern เดียวกับ CORR-VTRC-API-01 (transaction misuse) ที่พบใน audit
Running number generation
vtrc-api/api/src/lib/controllers/fund/fund.js:915-924
const fundRequestNoCondition = { where: { fundRequestNo: { [Op.like]: fundRequestNo + "%" } }, order: [["fundRequestNo", "DESC"]] }
const fundRequestDatas = await FundRequest.findByConditions(fundRequestNoCondition)
if (fundRequestDatas.length > 0) {
fundRequestNo += padLeadingZeros(fundRequestDatas[0].fundRequestOrder + 1, 6)
} else {
fundRequestNo += padLeadingZeros(1, 6)
}generate running number จาก SELECT ... ORDER BY ... LIMIT 1 แล้ว +1 — ถ้ามี concurrent insert 2 ตัวพร้อมกัน ทั้งคู่เห็น last = X → generate X+1 ซ้ำกัน → unique constraint error (ถ้ามี) หรือ row ซ้ำ (ถ้าไม่มี)
study/ (11 files, 2,008 LOC)
| File | LOC | Theme |
|---|---|---|
studyLeaveRequest.js | 1,282 | ใบลาศึกษาต่อ — main controller |
studyEducationFeeV2.js | 300 | ค่าเล่าเรียน V2 |
| อีก 9 ไฟล์ | ~426 รวม | sub-entity CRUD (detail, strategy, scholarship, ...) |
แต่ละ sub-entity มี pattern เดียวกัน — create*, update*, destroyByCondition, findByCondition, destroyByPkId เป็น CRUD scaffold ที่ generate จาก template เดียวกัน
study/studyEducationFeeV2.js:168 มี running-number race pattern เดียวกับ fund.js:915 — SELECT last + 1 โดยไม่มี lock
childTuition/ (59 LOC) และ conicle/ (305 LOC)
childTuition — 3 exports ทั้งหมดเป็น pass-through ไป centralize
conicle/ — authConicle login เข้า Conicle ด้วย cookie + csrf, sync course catalog จาก Conicle มา local Models.Conicle env var มี typo — CONINCLE_USERNAME (ตัว N เกิน) vs CONICLE_PAGE (prefix ไม่ตรงกัน) ใช้ใน config คนละจุด
Cluster F · Content & Comms
cluster ที่เก็บ content และ push communication ออกไปยัง user
post/ (272 LOC) และ document/ (696 LOC)
post/ — news + announcement — CRUD ปกติ + highLightPost (cap 3 active highlights per division)
document/ — fetchListDocumentByConditions, createDocument, highLightDocument มี documentDivision.js ที่ใช้ await arr.map(async ...) (ดูหัวข้อ bug ระบบ-wide ด้านล่าง)
course/, career/, contactUs/ (~585 LOC รวม)
CRUD pattern เดียวกันทั้งหมด — create*, update*, destroy*, highLight* + division scoping
notification/ (3 files, 779 LOC)
| File | LOC | Top exports |
|---|---|---|
notification.js | 716 | fetchListNotification, sendNotification (fan-out หลาย target) |
notificationRequest.js | 39 | bulk request |
queneNotification.js | 24 | queue helper (typo "quene" แทน "queue") |
จุดสังเกต — ReferenceError (QUAL-VTRC-API-05)
vtrc-api/api/src/lib/controllers/notification/notification.js:127-134
export const checkRequireNotification = async (context) => {
const notificationUserSetting = await findByConditions(context.userId)
if (notificationUserSetting != null) {
return requireNotification.requireNotification
} else {
return false
}
}function นี้อ้างถึง requireNotification.requireNotification โดยที่ไม่มี variable/module ชื่อนี้ใน scope ของไฟล์ (ไม่มี import, ไม่มี parameter) ถ้า code path นี้ถูกเรียกและเข้าเส้นทางที่ return → เกิด ReferenceError: requireNotification is not defined
sendNotification fan-out pattern
vtrc-api/api/src/lib/controllers/notification/notification.js:146-215
export const sendNotification = async (args) => {
let { statusContent, conditions } = await checkPublicOrInternalAndConditions(args)
if (statusContent === "INTERNAL") {
userInternal = await getUserNotificationInternal(args, conditions)
resultSendingInternalDeviceToken = await firebasePushNotification(args, ...)
} else if (statusContent === "ALLINTERNAL") {
// ...
} else {
// send noti all PUBLIC device and INTERNAL division
}
// ...
}sendNotification แยกเส้นทาง INTERNAL / ALLINTERNAL / PUBLIC+INTERNAL แล้วเรียก firebasePushNotification สองรอบ (public device + internal device) — เป็น fan-out manual ที่ถ้ารอบแรก success รอบสอง fail จะไม่ rollback log ของรอบแรก
createNotification/ (33 LOC)
vtrc-api/api/src/lib/controllers/createNotification/createNotification.js:1-34
export const createNoti = async (args, context) => {
// const t = await Models.db.transaction()
try {
if (args.notificationGroup === "MEETING") {
// ...
await notification.sendNotificationByThirdParty(...)
}
return { status: true }
} catch (error) {
console.log(error)
await t.rollback() // ← undefined
const errorCode = error.extensions ? error.extensions.code : null
if (errorCode !== null) {
log.error(...) // ← log ไม่ได้ import
} else {
log.error(...) // ← log ไม่ได้ import
responError('ERROR')
}
}
}สังเกตว่า transaction creation ถูก comment ไว้ (:4) แต่ catch block ยังเรียก t.rollback() (:24) — ถ้า error เกิดขึ้น → ReferenceError: t is not defined ซ้อนทับ error เดิม และ log ก็ไม่ได้ import → ReferenceError ซ้อนอีกชั้น สุดท้าย catch block ไม่มีวันทำงานถูกต้อง
sendEmail/ (80 LOC)
SMTP email — ใช้ nodemailer + config จาก Master_configs table
จุดสังเกต:
- subject มี
✔append ต่อท้ายเสมอ (:38) — hardcoded emoji ในโค้ด - error log ใช้ method name ผิด —
'Method:createStudyAttachment'(copy-paste จากไฟล์อื่น) ทำให้ log บอก method ผิด ตอน debug ต้อง cross-reference
Cluster G · Reports & File
cluster สำหรับ report generation + file storage + cache layer (ที่ตาย)
taxDraft/ (1,944 LOC)
| Export | Purpose |
|---|---|
templateFormTax | form schema ขนาดใหญ่ (~1,500 LOC เป็น JSON literal) |
fetchTaxDraft, createTaxDraft, updateTaxDraft | CRUD |
guideLineTax, confirmTextTaxDraft, detailTax | static content |
dropdownListRelation, validateInput | helpers |
~80% ของไฟล์เป็น form template literal — เหมาะกับย้ายไปเป็น i18n resource หรือ data-driven config แทนการ embed ใน controller
statLog/ (243 LOC)
cron ที่รันทุกวันที่ 00:30 — ดึง activation log จาก END_POINT_LOG_EVENT แล้ว bulk INSERT ลง db2 (ดู 3.1.8 สำหรับรายละเอียด cron schedule)
จุดสังเกต — SQL injection risk
vtrc-api/api/src/lib/controllers/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 ที่มี single-quote (เช่น '); DROP TABLE x;--) → SQL injection เข้า vtrc database ทางอ้อม ควรใช้ Sequelize replacements แทน string concat
excel/ (167 LOC + repository 614 LOC)
Excel export/import hub
| Export | Purpose |
|---|---|
downloadExcel | generic per-table download — รับ req.params.table เป็น dynamic |
createExportFile | FMIS Excel + KPayroll text + SmartCredit text + cash |
uploadExcel | read + insert |
จุดสังเกต — dynamic table (RCE equivalent)
downloadExcel รับ table parameter → ผ่านไป queryDataForExcelExport(table, condition) ใน repository → Models[table].findAll(...) ถ้า table มาจาก user input โดยตรง (ไม่ whitelist) = เทียบเท่า RCE เพราะ attacker สามารถอ่าน table อะไรก็ได้ใน DB (PII, password hash, token)
file/ (2 files, 366 LOC)
file.js (362 LOC) — saveFile, saveFilev2, getFileById, setUpFileBase64FromResolver, convertStreamFileToBase64File zip.js (4 LOC — debug leftover)
จุดสังเกต — hardcoded dev path
vtrc-api/api/src/lib/controllers/file/zip.js:4
return await generateZipFile("/Users/porch/Desktop/vtrc-api/api/storage")path ค้างจากเครื่อง developer ตัวเดิม (/Users/porch/Desktop/...) — ยัง exist ในโค้ดปัจจุบัน (verify 2026-07-13) ถ้าเรียกใช้จะ fail ในทุก environment อื่น เพราะ path นั้นไม่มีอยู่จริงบนเครื่องอื่น
file.js — stray token
vtrc-api/api/src/lib/controllers/file/file.js:21
const outStream = createWriteStream(tmpFile, { flags: "w" });
W // ← stray token, expression statement (harmless but copy-paste leftover)stray character W บรรทัดเดี่ยว — เป็น expression statement ที่ไม่ทำอะไร (evaluate แล้ว discard) แต่บ่งบอกว่ามี copy-paste ไม่สะอาด
redis.controller.js (19 LOC, root-level)
vtrc-api/api/src/lib/controllers/redis.controller.js:15-20
export const getCacheData = async (cacheKeyName, uid) => {
tr
const cacheId = buildDataCacheId(cacheKeyName, uid)
const data = await redisGet(cacheId)
return data
}tr บรรทัด 16 — เป็น token เดี่ยวที่ไม่ใช่ keyword ใดใน JavaScript ถ้า function นี้ถูกเรียก → ReferenceError: tr is not defined ยืนยันโค้ดยังเป็นแบบนี้อยู่ (verify 2026-07-13) — แต่ทุก caller ถูก comment หมดแล้ว (ดู 3.1.4) จึงไม่ crash จริงใน production
เป็นไฟล์เดี่ยวที่อยู่นอกโฟลเดอร์ cluster — ควรย้ายเข้า file/ หรือสร้าง cache/ หรือลบทิ้งถ้า dead code
Pattern ที่พบบ่อย (system-wide issues)
CORR-VTRC-API-01 · Transaction misuse
หลาย controller มี pattern ที่ประกาศ transaction แต่ไม่ commit/rollback ครบ chain:
// pattern — simplified
const t = await Models.db.transaction()
await Models.A.create({...}, { transaction: t })
await Models.B.create({...}) // ← ลืม { transaction: t }
await t.commit()กรณี B fail → A rollback แต่สิ่งที่ B สร้างใน chain อื่น (เช่น notification, file) ไม่ rollback พบใน withdraw/hospital.js, createNotification/createNotification.js
QUAL-VTRC-API-04 · await arr.map(async ...)
// ผิด — await ไม่ได้รอ Promise.all
await arr.map(async (item) => {
await Models.X.create(...)
})Array.map return array ของ Promise แต่ await array (ไม่ใช่ Promise) return ทันที — การ create ทุกตัวทำงาน parallel โดยไม่รอ และ error ใด error หนึ่งจะกลายเป็น unhandledRejection
ที่ถูก:
await Promise.all(arr.map(async (item) => {
await Models.X.create(...)
}))หรือใช้ for...of ถ้าต้องการ sequential
พบใน document/documentDivision.js, career/careerDivision.js, course/courseDivision.js, post/postDivision.js, contactUs/* — เป็น pattern ที่ copy กันจนกลายเป็น de-facto convention
QUAL-VTRC-API-05 · Fire-and-forget query
fund.js:926 เป็นตัวอย่างชัดเจน — function ไม่ async, เรียก Models.db.query() (return Promise) แต่ไม่ return ออกมา caller เรียกแบบ sync หาก query fail → silent rejection
พบใน cron-related controller (fund.js, statLog.js), notification dispatch (notification.js บาง branch)
Debug artifacts ที่ค้างในโค้ด
| ประเภท | ตำแหน่ง | Impact |
|---|---|---|
| Hardcoded dev path | file/zip.js:4 | ไม่ทำงานบนเครื่องอื่น |
| Stray token | file/file.js:21 (W), redis.controller.js:16 (tr) | ReferenceError ถ้าถูกเรียก |
| Artificial delay | slip/slip.js:78, slip/slip.js:307 | UX ช้า 3 วินาทีทุกครั้ง |
| Commented-out transaction + broken catch | createNotification/createNotification.js:4,24 | catch block พัง |
console.log debug | hrmi/hrmi.js (หลายจุด), slip/slip.js:319 | log noise ใน production |
| Unused import | hrmi/hrmi.js:28 (googleapis/build/src/apis/jobs), timeAttendance/timeAttendance.js (bcryptjs, jsonwebtoken) | cold start ช้า |
| Typo | auth/authSessionMoblie.js (Moblie), notification/queneNotification.js (quene), conicle env (CONINCLE_) | ค้นหายาก, refactor เสี่ยง |
Layer inversion — ที่ที่ controller ถูกเรียกกลับจาก repository
ปกติ arrow ลง — controller เรียก repository แต่มี 3 กรณีที่กลับด้าน
| Repository | Controller ที่ถูกเรียก | เหตุผล |
|---|---|---|
profile/profile.js | withdraw/hospital, hrmi/hrmi, masterData/approver | profile ต้องรู้ approval chain + leave context |
slip/slip.js | masterData/mapGroupDocument | salary cert eligibility check |
notification/notification.js | masterData/referenceData | join dropdown data |
ผล — testing ทั้ง 3 repo ต้อง mock controller ที่อยู่ข้างบน circular dependency ทำให้บางครั้ง Jest ต้องใช้ jest.mock() เพื่อ break cycle
เคล็ดลับตอนทำงานจริง
ถ้าจะเพิ่ม feature ใหม่ — เริ่มที่ไหน
- เขียน typeDefs ใน
typeDefs/{domain}.js(หรือไฟล์ใหม่) - เขียน resolver ใน
resolvers/{domain}.js - เขียน controller ใน
lib/controllers/{domain}/ - เขียน repository ใน
lib/repositories/{domain}/ - ถ้าต้องเรียก centralize → เขียน wrapper ใน
centralize/controllers/{domain}.js
ห้ามข้ามชั้น — ถ้า resolver เรียก Models ตรง หรือ controller เรียก typeDefs จะเละ
ถ้า debug "function นี้ทำงานผิด"
เริ่มที่ typeDefs → resolver → controller → repository → Models/centralize ตามลำดับ อย่ากระโดดไปแก้ controller ก่อนเข้าใจ resolver เพราะบางที bug อยู่ที่ resolver เลือก controller ผิด ไม่ใช่ controller เอง
await arr.map(async ...) — bug ระบบ-wide
// ผิด — await ไม่ได้รอ Promise.all
await arr.map(async (item) => {
await Models.X.create(...)
})ที่ถูก — await Promise.all(arr.map(async (item) => {...})) หรือใช้ for...of ถ้าต้องการ sequential
พบใน document/documentDivision.js, career/careerDivision.js, course/courseDivision.js, post/postDivision.js, contactUs/*
Controller ที่ยาวเกินไป — แบ่งอย่างไร
withdraw/hospital.js (3,179 LOC) เป็น candidate สำหรับแบ่งเป็น:
hospital.create.js— create path (createWDHospitalv2และ helper)hospital.update.js— update + state machine (updateWDHospital,getActionStatus,validateUpdateStatus)hospital.delegate.js— approval delegation (createDelegateApprover)hospital.list.js— list/fetch (fetchListWDHospital)hospital.report.js(มีแล้ว)hospital.template.js(มีแล้ว)
แต่การแบ่งกระทบ caller ทุกที่ — ต้อง coordinate import update และทำใน branch แยกเพื่อให้ review ได้
ตอนแก้ bug ใน controller — checklist
- อ่าน resolver ก่อนเพื่อรู้ว่า controller ถูกเรียกด้วย argument อะไร
- ตรวจว่ามี transaction wrap หรือเปล่า — ถ้ามี ต้องดูว่าทุก query ใช้
{ transaction: t }ครบไหม - ตรวจว่ามี
await arr.mapไหม — ถ้ามี ให้แก้เป็นPromise.all - ตรวจว่า catch block อ้าง variable ที่ import จริงไหม (เช่น
t,log) - ตรวจ hardcoded path / magic constant / debug
console.log - ตรวจว่า controller นี้ถูก layer inversion เรียกกลับจาก repository ไหม (ตารางด้านบน)