Skip to content

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 บรรทัด แต่เป็นศูนย์กลางทุกอย่าง

27:74:vtrc-api/api/src/index.js
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) ไว้ใน context ให้ resolver ใช้
  2. schemaDirectives: { auth: AuthDirective } — ลงทะเบียน directive ที่จะกลายเป็น gate #3-5
  3. playground: true + introspection: true — เปิดทั้ง production ทำให้ schema เปิดเผย จะกล่าวถึงใน บท 3.10
  4. debug: !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 เช่น

12:12:vtrc-api/api/src/config.js
  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>

41:84:vtrc-api/api/src/config.js
  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 ส่งเป็น apiKey header
  • APP_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)

150:155:vtrc-api/api/src/config.js
  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)

488:518:vtrc-api/api/src/config.js
  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 ภายใน (มี typo DRAF)
  • statusKcash — code ตอบกลับ KBANK K-Cash 6/7/8/10 → FAILED, 9 → SUCCESS

กฎเหล็ก — ค่าคงที่เหล่านี้ถือเป็น API ที่ frontend คาดหวัง เปลี่ยนแล้วกระทบหลายที่ ห้ามแก้ครั้งเดียว


Models registry — auto-load + syncModels = 79

ไฟล์ models/index.js (63 บรรทัด) ทำสองอย่าง

27:45:vtrc-api/api/src/models/index.js
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 / schema vtrc) — ไม่มี models/core/user
  • เดิน recursive เข้า centralize/ (User, UserDivision บน db2 / schema vtrc-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/Session8User (db2), Role, RoleAccess, Session, LoginSession, CaptchaSession, UserDivision, AuthSession
Content12Post, Document, Course, Career, ContactUs, Notification, Policy + variants
Leave4LeaveDocument, LeaveHistory, Approver, Delegate
Withdraw/Hospital8WDHospital, WDSlip, WDHistory, WDExport, WDHospitalDocument, Hospital, Sickness, MasterApprover
Fund8FundApplication, FundRequest, FundRateChange, FundBenefitPerson, FundDocument, FundRequestHistory
Study Leave11StudyLeaveRequest, StudyLeaveDetail, StudyStrategy, StudyEducationFee, StudyExtend, ...
File + Master10File, MasterConfig, MasterDropDown, ReferenceData, Bank, BankLog, HospitalBankAccount, VTRCBankAccount
Miscที่เหลือใน syncModelsAddress, 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:34
  • nameEN vs nameEn (case ต่างกัน) — config.js:323 และ :335
  • MaterData (ผิดจาก Master) — config.js:309
  • DRAF (ผิดจาก DRAFT) — config.js:484
  • Moblie (ผิดจาก Mobile) — authSessionMoblie.js

ทั้งหมดเป็น public API แล้ว — ห้ามแก้ครั้งเดียว


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

ไป บท 3.2 GraphQL request pipeline