3.1.1 · Repository map + boot sequence
บทนี้เล่าว่า vtrc-api มีโครงสร้างไฟล์อย่างไร และ process เริ่มต้นอย่างไรตั้งแต่ node index.js จนถึงพร้อมรับ request บทถัดไปทั้งหมดอ้างชื่อไฟล์จากแผนที่ในบทนี้
แผนที่โฟลเดอร์ (api/src/)
api/src/
├── index.js 120 บรรทัด — entry point: Express + ApolloServer + listen()
├── config.js 522 บรรทัด — env var + device key registry + business constant tables
├── connector.js 50 บรรทัด — Sequelize instance คู่ db + db2
├── crons.js 72 บรรทัด — node-schedule jobs (3 ตัว active) + APP_NO leader election
├── defaultData.js seed data สำหรับ model sync ครั้งแรก
├── routes.js 1,301 บรรทัด — REST routes คู่กับ GraphQL
├── typeDefs/ 44 ไฟล์ — GraphQL schema ทั้งหมด เขียนด้วยมือ
├── resolvers/ 41 ไฟล์ + index.js — mirror typeDefs, Query/Mutation mapping
├── directives/ auth directive (auth.js, 51 บรรทัด) + index
├── models/
│ ├── index.js 63 บรรทัด — auto-loader เดิน recursive เข้า core/ + centralize/
│ ├── core/ 25 โฟลเดอร์ — ตารางใน schema {vtrc} ผ่าน db
│ └── centralize/ 1 โฟลเดอร์ (user) — ตารางใน schema {vtrc-centralize} ผ่าน db2
├── lib/
│ ├── controllers/ 31 โฟลเดอร์ — orchestration layer
│ ├── repositories/ 31 โฟลเดอร์ — data access layer
│ ├── model.js 137 บรรทัด — syncModelToTable() + createDatabases()
│ ├── redis.js Redis connection + helper
│ ├── responErrors.js error code → Thai message mapping
│ ├── wlog.js structured file logger
│ ├── logging.js GraphQL plugin logger
│ ├── encrypt/ EncryptAll() — bare hash helper
│ ├── firebase/ FCM push notification
│ ├── file.js file storage helper (legacy path)
│ ├── date.js moment timezone helpers
│ └── createslip/ PDF pipeline (slip + tax + failpayment + genslip)
└── centralize/ ⚡ BRIDGE LAYER ไปยัง cu-central-api
├── controllers/ 17 ไฟล์, 4,166 บรรทัด — 1 ต่อ domain (hrmi, slip, profile, ...)
└── graphql/ queries.js (2,172 บรรทัด) + mutations.js (376 บรรทัด)
fonts/ PDF fonts (TH Sarabun)
keys/ RSA keypair สำหรับ loginSession (1 ชุดต่อ frontend)
pdfTemplate/ EJS templates สำหรับ PDF อื่นๆ (withdraw, fund)
report-template.ejs slip PDF template
tax-template.ejs tax PDF template
payment-template.ejs bank payment PDF templateจำง่าย — "typeDefs + resolvers อยู่ติดกัน, lib/controllers + lib/repositories อยู่ใต้ lib/, centralize/ อยู่ต่างหากเพราะเป็นขอบเขตพิเศษ"
ทุกตัวเลขข้างบน verify ใหม่จาก filesystem จริง (2026-07-13) — ต่างจาก v1 ที่นับ typeDefs 43 (จริง 44), controllers 30 (จริง 31), repositories 32 (จริง 31), models/core 27 (จริง 25) ดู index — การเปลี่ยนแปลงจาก v1
Boot sequence
เมื่อรัน npm start (จริงคือ node -r esm .) ลำดับการทำงานคือ
1. config.js โหลด dotenv + destructure env var (ทุกตัวมี default)
│
▼
2. connector.js สร้าง Sequelize instance db (บรรทัด 8-27) + db2 (บรรทัด 29-48)
│
▼
3. models/index.js auto-load model files เข้า Models registry
(core/ → db, centralize/ → db2; User อยู่เฉพาะ centralize/db2)
│
▼
4. crons.js (import side-effect) register cron jobs
(fund-rate job เฉพาะ APP_NO === 1)
│
▼
5. index.js สร้าง ApolloServer + Express app, register routes + middleware
│
▼
6. app.listen(PORT) เริ่มรับ HTTP request (index.js:98)
│
├─► redisConnection() Redis connect (fire-and-forget, index.js:100)
│
├─► db.authenticate() MariaDB #1 connect
│ └─► syncModelToTable() sync 79 model active → DB schema (index.js:104)
│
└─► db2.authenticate() MariaDB #2 connect
└─► createDatabases(db2) CREATE DATABASE IF NOT EXISTSจุดสำคัญ — syncModelToTable() รันทุกครั้งที่ boot (vtrc-api/api/src/index.js:104) วนลูปตามรายการ syncModels ที่ยังไม่ถูก comment (79 ตัว, vtrc-api/api/src/lib/model.js:17-95) — schema drift เกิดอย่างเงียบทุกครั้งที่ deploy ไม่ครบทุก environment พร้อมกัน ดู 3.1.5 Persistence
Entry point: index.js
ไฟล์ vtrc-api/api/src/index.js ยาว 120 บรรทัด เป็นศูนย์กลางทุกอย่าง
vtrc-api/api/src/index.js:27-74
const server = new ApolloServer({
typeDefs,
resolvers,
uploads: false,
context: async ({ req, res }) => {
try {
const apiKey = req.headers.apikey
const mapKey = await allowedKeys.filter(value => value.key === apiKey)
if (mapKey.length === 0) {
responError('ACCESS_KEY_INVALID')
}
// get the user token from the headers
const tokenWithBearer = req.headers.authorization || ''
const token = tokenWithBearer.split(' ')[1]
const { loggedIn, payload: user } = await getPayLoad(token)
return {
token,
userId: user.uid,
loggedIn,
device: mapKey[0].APP_TYPE,
appId: mapKey[0].id,
headers: req.headers
}
} catch (err) {
console.log("err ->", err);
log.error(log.logTextFormat('CONTEXT ERROR', 'Method:context', 'ERROR', err, { ...req.headers }))
responError("ERROR")
}
},
formatError: (err) => {
console.log("err ->", err);
return IS_PRODUCTION ? graphqlFormatError(err) : err
},
schemaDirectives: {
auth: AuthDirective
},
playground: true,
introspection: true,
debug: !IS_PRODUCTION,
plugins: [
() => new GraphQLLogging()
]
})สังเกต 4 จุดสำคัญ
context()คือ gate #1 และ gate #2 — ตรวจ apiKey ก่อน (gate #1) แล้วถอด JWT ดู uid (gate #2)schemaDirectives: { auth: AuthDirective }— ลงทะเบียน directive ที่จะกลายเป็น gate #3-5playground: true+introspection: true— เปิดทั้ง production ทำให้ schema เปิดเผยต่อ public (severity ดู 3.1.10)debug: !IS_PRODUCTION— stack trace ปิดใน prod แต่ playground ยังเปิดอยู่
config.js — ไฟล์ที่บรรจุ "นโยบาย" ทั้งหมด
ไฟล์ config.js (522 บรรทัด) ไม่ใช่แค่ config ทั่วไป — เป็นที่เก็บ business rules hardcoded แบ่งเป็น 4 ก้อน
ก้อนที่ 1 — env var (บรรทัด 1-40)
vtrc-api/api/src/config.js:12-12
JWT_SECRET = 'secret',ถ้า env ไม่ override → ใครก็ forge JWT ได้ (severity V3-S2 ใน 3.1.10)
ก้อนที่ 2 — Device key registry (เริ่มบรรทัด 41)
vtrc-api/api/src/config.js:41
API_DEVICE_KEY = {registry ของ apiKey ที่ frontend ส่งมาใน HTTP header apiKey: {uuid} มี 2 tier คือ prod และ uat
keyคือสิ่งที่ frontend ส่งเป็นapiKeyheaderAPP_TYPEคือ device class — axis หนึ่งของ RBAC (คู่กับ role)- key เหล่านี้ committed ใน git เป็น plaintext UUID ที่ไม่ rotate (severity H2)
- lookup เป็น
Array.filterเส้นตรง — O(N) ต่อ request ไม่ใช่ปัญหาที่ N เล็ก แต่เป็น code smell
ก้อนที่ 3 — Access + menu overlay (เริ่มบรรทัด 150)
vtrc-api/api/src/config.js:150-154
ACCESS_BACK_OFFICE = ['SUPER_ADMIN', 'ADMIN', 'STAFF'],
ACCESS_USER = ['USER'],
KEY_CU_H = 'CU-RE-STRUCTURE-ID-02',
KEY_PLASMA = '47837e2d-7788-4a1a-8e24-a90abf6742f4',
KEY_HR = ['305A814A-CEAC-449B-BF92-CCAEC3475B1A', '5A764C0F-6C0D-46C5-83E0-9B94C755BFE2'],ACCESS_BACK_OFFICE/ACCESS_USER— role หลัก 4 ตัวKEY_HR— UUID ของบทบาท HR พิเศษที่ bypass rule บางอย่างMENUS_BACK_OFFICE— menu registry เป็น axis ที่ 3 ของ RBAC (คู่กับ role + device) ดู 3.1.3
ก้อนที่ 4 — Business constant tables (เริ่มบรรทัด 371)
vtrc-api/api/src/config.js:488
statusKcash = [relationHrmi— code ความสัมพันธ์ 00, 10, 20, 70, 81-85statusHrmi— code W/Y/N/C/CR/A/D → status ภายใน (มี typoDRAF)statusKcash— code ตอบกลับ KBANK (ธนาคารกสิกรไทย — ไม่ใช่กรุงเทพ ตาม canonical truth) K-Cash — id'9'→ statusSUCCESS
กฎเหล็ก — ค่าคงที่เหล่านี้ถือเป็น API ที่ frontend คาดหวัง เปลี่ยนแล้วกระทบหลายที่ ห้ามแก้ครั้งเดียว
Models registry — auto-load + syncModels = 79
ไฟล์ models/index.js (63 บรรทัด) เดิน recursive เข้า core/ (25 โฟลเดอร์ บน db) แล้วเข้า centralize/ (1 โฟลเดอร์ user บน db2)
vtrc-api/api/src/models/index.js:27-45
walk(path.join(__dirname, "core"))
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')
})
.forEach(file => {
const model = require(path.join(__dirname, file)).default(db, Sequelize.DataTypes);
Models[model.name] = model
})
//[CETRANLIZE] database center
walk(path.join(__dirname, "centralize"))
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')
})
.forEach(file => {
const model = require(path.join(__dirname, file)).default(db2, Sequelize.DataTypes);
Models[model.name] = model
})ไม่มี models/core/user/ ใน repo (ยืนยันจาก ls models/core/ — 25 โฟลเดอร์ ไม่มีชื่อ user) — Models.User มาจาก models/centralize/user/user.js เท่านั้น จึงเขียนอยู่บน schema vtrc-centralize (db2) ไม่ใช่ vtrc
จากนั้นรัน associate() และ addIndex() ให้ทุก model (models/index.js:47-56) และ inject Models.db, Models.db2, Models.Op, Models.Sequelize เข้า registry เป็น globals — code ทุกที่เขียน Models.WDHospital.findAll(...) ได้โดยไม่ต้อง import model ตรง (terse แต่ test ยาก)
ขนาดของ registry
syncModels list ใน lib/model.js:17-95 มี 79 ชื่อที่ยัง active (comment ออก 2 ตัว) — คือจำนวนที่ syncModelToTable() sync จริงทุก boot
| กลุ่ม | ตัวอย่าง |
|---|---|
| Identity/Session | User (db2), Role, RoleAccess, Session, LoginSession, CaptchaSession, UserDivision, AuthSession |
| Content | Post, Document, Course, Career, ContactUs, Notification, Policy + variants |
| Leave | LeaveDocument, LeaveHistory, Approver, Delegate |
| Withdraw/Hospital | WDHospital, WDSlip, WDHistory, WDExport, WDHospitalDocument, Hospital, Sickness, MasterApprover |
| Fund | FundApplication, FundRequest, FundRateChange, FundBenefitPerson, FundDocument, FundRequestHistory |
| Study Leave | StudyLeaveRequest, StudyLeaveDetail, StudyStrategy, StudyEducationFee, StudyExtend, ... |
| File + Master | File, MasterConfig, MasterDropDown, ReferenceData, Bank, BankLog, HospitalBankAccount, VTRCBankAccount |
| Misc | Address, TaxDraft, Version, Goals, Strategy, Output, ... |
เคล็ดลับตอนทำงานจริง
ห้ามหลงเข้าไปใน dist/
api/src/dist/ เป็น compiled output ของบางสคริปต์ — exclude ตอน grep/search ไม่งั้นเจอโค้ดซ้อนกันสองชั้น
แยกแยะ Models.User สองตัว
Models.User (จาก models/centralize/user/user.js, เชื่อม db2) เป็น cross-schema join table ไม่ใช่ตัวเดียวกับ centralize-api/entities/user.entity.ts (MSSQL auth_user — คนละ service) ดู 3.1.5
ระวัง typo ที่ติดมาจากประวัติศาสตร์ (public API แล้ว — ห้ามแก้ครั้งเดียว)
CONINCLE_USERNAME(ผิดจากCONICLE) —config.jsnameENvsnameEn(case ต่างกัน) —config.jsDRAF(ผิดจากDRAFT) —config.js:484(statusHrmi block)Moblie(ผิดจากMobile) —authSessionMoblie.js