3.1 · Repository map + boot sequence
บทนี้เล่าว่า vtrc-api มีโครงสร้างเป็นอย่างไรเวลามองจาก "ไฟล์" และ process เริ่มต้นอย่างไรตั้งแต่ node index.js จนถึงพร้อมรับ request
ถ้าจะเข้าใจบทถัดไป (pipeline, auth, persistence) ต้องผ่านบทนี้ก่อน — เพราะทุกบทจะอ้างชื่อไฟล์ที่อยู่ในแผนที่ตรงนี้
แผนที่โฟลเดอร์ (สิ่งที่อยู่ใน api/src/)
api/src/
├── index.js entry point — Express + ApolloServer + listen()
├── config.js 14 KB — env var + device key registry + business constant tables
├── connector.js Sequelize instance ตัวเดียวสำหรับ db + db2
├── crons.js node-schedule jobs (5 ตัว) + APP_NO leader election
├── defaultData.js seed data สำหรับ model sync ครั้งแรก
├── routes.js 1,302 บรรทัด — REST routes คู่กับ GraphQL
├── typeDefs/ 43 ไฟล์ — GraphQL schema ทั้งหมด เขียนด้วยมือ
├── resolvers/ 43 ไฟล์ mirror typeDefs — Query/Mutation mapping
├── directives/ auth directive (1 ไฟล์จริง) + index
├── models/ Sequelize models
│ ├── index.js auto-loader เดิน recursive เข้า core/ + centralize/
│ ├── core/ 27 โฟลเดอร์ — ตารางใน schema {vtrc}
│ └── centralize/ 1 โฟลเดอร์ — ตารางใน schema {vtrc-centralize}
├── lib/
│ ├── controllers/ 30 โฟลเดอร์ — orchestration layer
│ ├── repositories/ 32 โฟลเดอร์ — data access layer
│ ├── model.js syncModelToTable() + createDatabases()
│ ├── redis.js Redis connection + helper
│ ├── responErrors.js 14 KB — error code → Thai message mapping
│ ├── wlog.js structured 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 ไฟล์ — 1 ต่อ domain (hrmi, slip, profile, ...)
└── graphql/ queries.js + mutations.js — GraphQL strings ทั้งหมด
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/ อยู่ต่างหากเพราะเป็นขอบเขตพิเศษ"
Boot sequence — อะไรเกิดก่อนอะไร
เมื่อรัน npm start (แท้จริงแล้วคือ node -r esm .) ลำดับการทำงานคือ
1. config.js โหลด dotenv + destructure env var (ทุกตัวมี default)
│
▼
2. connector.js สร้าง Sequelize instance db + db2 (ยังไม่ได้ connect)
│
▼
3. models/index.js auto-load model files เข้า Models registry
(core → db, centralize → db2; User อยู่เฉพาะ centralize/db2)
(รวมถึง associate และ addIndex)
│
▼
4. crons.js (import side-effect) register 5 cron jobs
(ถ้า APP_NO === 1 เท่านั้น)
│
▼
5. index.js สร้าง ApolloServer + Express app
register routes + middleware
│
▼
6. app.listen(PORT) เริ่มรับ HTTP request
│
├─► redisConnection() Redis connect (fire-and-forget)
│
├─► db.authenticate() MariaDB #1 connect
│ └─► syncModelToTable() sync 79 model (active ใน syncModels) → DB schema
│
└─► db2.authenticate() MariaDB #2 connect
└─► createDatabases(db2) CREATE DATABASE IF NOT EXISTSจุดสำคัญ — syncModelToTable() รันทุกครั้งที่ boot (api/src/index.js:104) ลึกลงไปจะเจอ Models[modelName].sync({ force: false }) ที่รันเป็น loop ตามรายการ syncModels ที่ยังไม่ถูก comment (79 ตัว, api/src/lib/model.js:17-99) — หมายความว่า schema drift เกิดอย่างเงียบ ทุกครั้งที่ deploy ไม่ตรงกัน ดู บท 3.5 สำหรับรายละเอียด
Entry point: index.js ใน 1 รอบ
ไฟล์ api/src/index.js ยาวแค่ 121 บรรทัด แต่เป็นศูนย์กลางทุกอย่าง
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) ไว้ใน context ให้ resolver ใช้schemaDirectives: { auth: AuthDirective }— ลงทะเบียน directive ที่จะกลายเป็น gate #3-5playground: true+introspection: true— เปิดทั้ง production ทำให้ schema เปิดเผย จะกล่าวถึงใน บท 3.10debug: !IS_PRODUCTION— stack trace ปิดใน prod แต่ playground ยังเปิดอยู่
config.js — ไฟล์ที่บรรจุ "นโยบาย" ทั้งหมด
ไฟล์ config.js (520 บรรทัด) ไม่ใช่แค่ config ทั่วไป — เป็นที่เก็บ business rules ที่ hardcoded ในระบบ แบ่งเป็น 4 ก้อน
ก้อนที่ 1 — env var (บรรทัด 1-40)
ทุก env var มี default ทำให้รัน local ได้ทันที แต่บาง default เป็นเหมือนกับโมเมะที่ไม่ควรอยู่ใน production เช่น
JWT_SECRET = 'secret',ถ้า env ไม่ override → ใครก็ forge JWT ได้ (severity S5 ใน บท 3.10)
ก้อนที่ 2 — Device key registry (บรรทัด 41-149)
เป็น registry ของ apiKey ที่ frontend ส่งมา มี 2 tier คือ prod (7 key) และ uat (10 key) โดยที่ frontends ส่งค่ามาใน HTTP header apiKey: <uuid>
API_DEVICE_KEY = {
prod: [
{
id: '93361f06-6f55-11eb-9439-0242ac130002',
key: 'fd77b56f-1489-4fb1-a094-a614f0d16198',
APP_TYPE: 'WEB',
APP_NAME: 'VTRC-WEB'
},
...จุดที่ต้องรู้
keyคือสิ่งที่ frontend ส่งเป็นapiKeyheaderAPP_TYPEคือ device class — เป็น axis หนึ่งของ RBAC (คู่กับ role)- key เหล่านี้ committed ใน git เป็น plaintext UUID ที่ไม่ rotate ใครได้ repo access ก็ impersonate แอปได้ทันที (severity H2)
- การ lookup เป็น
Array.filterเส้นตรง (index.js:34) — O(N) ต่อ request ไม่ใช่ปัญหาที่ N=7 แต่เป็น code smell
ก้อนที่ 3 — Access + menu overlay (บรรทัด 150-370)
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'],
CONICLE_PAGE = ['MyLibrary', 'LearningRequest', 'Home'],ACCESS_BACK_OFFICE/ACCESS_USER— role หลัก 4 ตัวKEY_HR— UUID ของบทบาท HR พิเศษที่ bypass rule บางอย่างMENUS_BACK_OFFICE— menu registry ~32 ตัว เป็น axis ที่ 3 ของ RBAC (คู่กับ role + device) — จะอธิบายใน บท 3.3
ก้อนที่ 4 — Business constant tables (บรรทัด 371-519)
statusKcash = [
{
id: '6',
label: 'ISSUED and/or STOPPAY WITH DUPLICATE',
labelTH: 'การดำเนินการและ/หรือหยุดการจ่ายจากการทำซ้ำ',
status: 'FAILED',
},
...relationHrmi— code ความสัมพันธ์ 00, 10, 20, 70, 81-85 (พ่อ แม่ คู่สมรส บุตร 1-5) — cap 5 บุตรstatusHrmi— code W/Y/N/C/CR/A/D → status ภายใน (มี typoDRAF)statusKcash— code ตอบกลับ KBANK K-Cash 6/7/8/10 → FAILED, 9 → SUCCESS
กฎเหล็ก — ค่าคงที่เหล่านี้ถือเป็น API ที่ frontend คาดหวัง เปลี่ยนแล้วกระทบหลายที่ ห้ามแก้ครั้งเดียว
Models registry — auto-load + syncModels = 79
ไฟล์ models/index.js (63 บรรทัด) ทำสองอย่าง
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
})- เดิน recursive เข้า
core/(หลาย domain บนdb/ schemavtrc) — ไม่มีmodels/core/user - เดิน recursive เข้า
centralize/(User,UserDivisionบนdb2/ schemavtrc-centralize) —Models.Userจึงมาจากmodels/centralize/user/user.jsเท่านั้น ไม่ใช่การทับ core User
จากนั้นรัน associate() และ addIndex() ให้ทุก model (models/index.js:48-56) และ inject Models.db, Models.db2, Models.Op, Models.Sequelize เข้า registry เป็น globals
ผลคือ — code ทุกที่สามารถเขียน Models.WDHospital.findAll(...) หรือ Models.db.transaction(...) ได้โดยไม่ต้อง import ไฟล์ model โดยตรง เป็นทั้งข้อดี (terse) และข้อเสีย (test ยาก)
ขนาดของ registry
syncModels list ใน lib/model.js:17-99 มี 79 ชื่อที่ยัง active (ไม่นับที่ comment ออก เช่น 'BankBranch', 'BankConfigExportCode') — นี่คือจำนวนที่ syncModelToTable() sync จริงทุก boot
โดยกลุ่ม
| กลุ่ม | จำนวนโมเดล (ประมาณ) | ตัวอย่าง |
|---|---|---|
| Identity/Session | 8 | User (db2), Role, RoleAccess, Session, LoginSession, CaptchaSession, UserDivision, AuthSession |
| Content | 12 | Post, Document, Course, Career, ContactUs, Notification, Policy + variants |
| Leave | 4 | LeaveDocument, LeaveHistory, Approver, Delegate |
| Withdraw/Hospital | 8 | WDHospital, WDSlip, WDHistory, WDExport, WDHospitalDocument, Hospital, Sickness, MasterApprover |
| Fund | 8 | FundApplication, FundRequest, FundRateChange, FundBenefitPerson, FundDocument, FundRequestHistory |
| Study Leave | 11 | StudyLeaveRequest, StudyLeaveDetail, StudyStrategy, StudyEducationFee, StudyExtend, ... |
| File + Master | 10 | File, MasterConfig, MasterDropDown, ReferenceData, Bank, BankLog, HospitalBankAccount, VTRCBankAccount |
| Misc | ที่เหลือใน syncModels | Address, TaxDraft, Version, Goals, Strategy, Output, ... |
ทำไมต้องรู้เรื่องนี้ก่อน
บทถัดไปทุกบทจะอ้างชื่อไฟล์จากแผนที่ตรงนี้ — เวลาเจอ "auth check อยู่ที่ไหน" จะได้รู้ว่าไปดูที่ directives/auth.js + lib/repositories/session/session.js
ถ้ายังไม่คุ้นชื่อไฟล์ — กลับมาดูแผนที่ในบทนี้ก่อน
เคล็ดลับตอนทำงานจริง
ห้ามหลงเข้าไปใน dist/
มีโฟลเดอร์ api/src/dist/ ที่เป็น compiled output ของบางสคริปต์ — ไม่ใช่ source จริง เวลาค้นหาด้วย IDE ให้ exclude dist/ ไม่งั้นจะเจอโค้ดซ้อนกันสองชั้น
แยกแยะ Models.User สองตัว
มี Models.User ที่อยู่ใน models/centralize/user/user.js (เชื่อมกับ db2) — เป็น cross-schema join table ไม่ใช่ตัวเดียวกับ centralize-api/entities/user.entity.ts (MSSQL auth_user) ดู บท 3.5 และ Volume 9 Data Model สำหรับ ERD
config.js ไม่ใช่ที่เดียวที่เก็บค่าคงที่
บาง business rule ฝังอยู่ใน model ENUM (เช่น PEDDING_CANCLE ใน models/core/leave/leave.js:59) หรือใน controller inline (เช่น leave-day limit ใน lib/controllers/hrmi/hrmi.js:1295-1319) ดู Volume 12 Business Logic สำหรับรวบรวม
ระวัง typo ที่ติดมาจากประวัติศาสตร์
CONINCLE_USERNAME(ผิดจากCONICLE) —config.js:34nameENvsnameEn(case ต่างกัน) —config.js:323และ:335MaterData(ผิดจากMaster) —config.js:309DRAF(ผิดจากDRAFT) —config.js:484Moblie(ผิดจากMobile) —authSessionMoblie.js
ทั้งหมดเป็น public API แล้ว — ห้ามแก้ครั้งเดียว