Skip to content

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

javascript
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 จุดสำคัญ

  1. context() คือ gate #1 และ gate #2 — ตรวจ apiKey ก่อน (gate #1) แล้วถอด JWT ดู uid (gate #2)
  2. schemaDirectives: { auth: AuthDirective } — ลงทะเบียน directive ที่จะกลายเป็น gate #3-5
  3. playground: true + introspection: true — เปิดทั้ง production ทำให้ schema เปิดเผยต่อ public (severity ดู 3.1.10)
  4. 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

javascript
  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

javascript
  API_DEVICE_KEY = {

registry ของ apiKey ที่ frontend ส่งมาใน HTTP header apiKey: {uuid} มี 2 tier คือ prod และ uat

  • key คือสิ่งที่ frontend ส่งเป็น apiKey header
  • APP_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

javascript
  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

javascript
  statusKcash = [
  • relationHrmi — code ความสัมพันธ์ 00, 10, 20, 70, 81-85
  • statusHrmi — code W/Y/N/C/CR/A/D → status ภายใน (มี typo DRAF)
  • statusKcash — code ตอบกลับ KBANK (ธนาคารกสิกรไทย — ไม่ใช่กรุงเทพ ตาม canonical truth) K-Cash — id '9' → status SUCCESS

กฎเหล็ก — ค่าคงที่เหล่านี้ถือเป็น 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

javascript
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/SessionUser (db2), Role, RoleAccess, Session, LoginSession, CaptchaSession, UserDivision, AuthSession
ContentPost, Document, Course, Career, ContactUs, Notification, Policy + variants
LeaveLeaveDocument, LeaveHistory, Approver, Delegate
Withdraw/HospitalWDHospital, WDSlip, WDHistory, WDExport, WDHospitalDocument, Hospital, Sickness, MasterApprover
FundFundApplication, FundRequest, FundRateChange, FundBenefitPerson, FundDocument, FundRequestHistory
Study LeaveStudyLeaveRequest, StudyLeaveDetail, StudyStrategy, StudyEducationFee, StudyExtend, ...
File + MasterFile, MasterConfig, MasterDropDown, ReferenceData, Bank, BankLog, HospitalBankAccount, VTRCBankAccount
MiscAddress, 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.js
  • nameEN vs nameEn (case ต่างกัน) — config.js
  • DRAF (ผิดจาก DRAFT) — config.js:484 (statusHrmi block)
  • Moblie (ผิดจาก Mobile) — authSessionMoblie.js

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

ไป 3.1.2 GraphQL request pipeline