Skip to content

2.3 · Request lifecycle

บทนี้ trace request หนึ่งครั้งตั้งแต่ user click จนกระทั่งเห็นผลบนหน้าจอ — เพื่อให้รู้ว่าแต่ละจุดทำอะไร และตอน debug ต้องไปดูที่ไหน

ขอใช้ตัวอย่างที่ครอบคลุมทุก hop — พนักงานเปิดหน้าสลิปเงินเดือน


แผนภาพรวม

1. พนักงานเปิดหน้า "สลิปเงินเดือน" ใน vtrc-web


2. vtrc-web สร้าง GraphQL query พร้อม headers
       • Authorization: Bearer <jwt>
       • apiKey: <base64 device key>

       │  POST https://vtrcapi.redcross.or.th/api/graphql

3. Nginx รับ HTTPS → terminate TLS → forward HTTP ไปยัง upstream
       • ไม่มี auth ไม่มี routing logic
       • load balance implicit (DNS/ports)

       │  HTTP → http://localhost:8081 (หรือ 8082/8083/8085)

4. vtrc-api (Apollo Server) รับ request


5. context() ทำงานก่อน resolver
       • อ่าน apiKey header → checkApiKey → ได้ APP_TYPE
       • แยก Bearer token → getPayLoad(token) → ได้ { user, loggedIn }
       • return { userId, loggedIn, device, appId, headers }


6. @auth directive ตรวจสอบสิทธิ์
       • verifyToken (JWT decode + check expiry + issuer + jti)
       • checkSession (query MariaDB Session table)
       • checkMenuAuth (query RoleAccess)


7. Resolver ทำงาน (เช่น fetchPaySlip)


8. Controller ใน lib/controllers/slip/
       • เรียก centralize bridge

       │  GraphQL request → cu-central-api (END_POINT_CENTRALIZE; ตัวอย่าง env :8082 / CODE_DEFAULT 8080)

9. cu-central-api (Node 12 + Apollo)
       • resolver + @auth directive ฝั่ง cu-central
       • Sequelize query → MSSQL HRMI


10. MSSQL คืน rows → cu-central-api → vtrc-api → resolver → frontend


11. vtrc-web render สลิปบนจอ

หมายเหตุEND_POINT_CENTRALIZE ใน vtrc-api ชี้ไป cu-central-api GraphQL ไม่ใช่ NestJS centralize-api (port 3000, REST) — แยกพอร์ต: CODE_DEFAULT APP_PORT=8080, CLIENT_TARGET ใน .env.example อาจเป็น 8082 ดู บท 3.4


รายละเอียดแต่ละ hop

Hop 1-2 · Frontend

vtrc-web/src/config/apollo.js:48-67authLink แปะ headers ทุก request

js
// ตัวอย่างสรุป ไม่ใช่โค้ดจริงทั้งหมด
authLink.setRequest((operation, forward) => {
  const token = getToken()  // จาก localStorage
  operation.setContext({
    headers: {
      Authorization: token ? `Bearer ${token}` : "",
      apiKey: atob(apiKeyNews())  // ถอดรหัส base64 ตอนส่ง
    }
  })
  return forward(operation)
})

ข้อสังเกตvtrc-web ใช้ atob() ถอด base64 ก่อนส่ง apiKey (vtrc-web/src/config/apollo.js:55) แต่ vtrc-rc-backoffice ส่งโดยตรง ไม่ถอด (vtrc-rc-backoffice/src/config/apollo.js:55) — encoding convention ไม่เหมือนกัน ถ้า copy-paste logic ระหว่าง 2 repo ต้องระวัง


Hop 3 · Nginx

Nginx ทำ 2 อย่างเท่านั้น

  1. TLS termination (*:443 → HTTP)
  2. Reverse proxy ไปยัง vtrc-api upstream

ไม่มี rate limit ไม่มี auth ไม่มี rewrite ไม่มี caching header แบบ custom — ทุกอย่างอยู่ที่ฝั่งแอป

vtrc-api/docker-compose.yml:14-34 — nginx container ใช้ network_mode: host จึงเห็นทุก port ของ host


Hop 4-5 · context() ใน Apollo

vtrc-api/api/src/index.js:31-60 — context function ทำงานก่อน resolver ทุกตัว

1. อ่าน apiKey header
2. filter allowedKeys จาก API_DEVICE_KEY (built once at module load)
3. ถ้าไม่ match → throw ACCESS_KEY_INVALID
4. แยก Bearer token จาก Authorization header
5. getPayLoad(token) → { loggedIn, user }
6. return { token, userId, loggedIn, device, appId, headers }

ถ้า context throw → resolver ไม่ทำงาน เพราะ Apollo ไม่เรียก

ข้อสังเกตapiKey ไม่ใช่ auth แท้จริง เพราะเป็น device key ที่ hardcode ใน config.js:41-149 ใครก็สามารถ extract จาก frontend bundle ได้ มันใช้แค่บอกว่า request มาจาก device ประเภทไหน (WEB, MOBILE, WEB_ADMIN, ...)


Hop 6 · @auth directive

vtrc-api/api/src/directives/auth.js:1-52 — ทำงาน 4 ขั้น

1. ตรวจ token มีไหม → ถ้าไม่มี throw TOKEN_INVALID
2. verifyToken (jwt.verify + expiry + issuer + jti)
3. checkSession(userId, role, jti, device)
     → query MariaDB Session where userId, userSession LIKE %jti%, client = device
     → ถ้า expireDate <= now → throw
4. checkMenuAuth(userId, accessMenu)
     → query RoleAccess rows
     → ถ้า role.nameEN ไม่อยู่ใน accessMenu → throw "คุณไม่สิทธิดำเนินการ"

ข้อสังเกตuserSession LIKE %jti% (vtrc-api/api/src/lib/repositories/session/session.js:20-48) เป็น substring match เพราะ column เก็บ JSON object ที่มี jwtId อยู่ข้างใน ไม่ได้ index จึง query ช้าถ้ามี session มาก


Hop 7-8 · Resolver + Controller

resolvers/slip.js → lib/controllers/slip/slip.js → centralize bridge

resolver เป็น thin orchestrator — เรียก controller ใน lib/controllers/ controller เป็นที่ที่ business logic อยู่จริง

controller เรียก centralize/controllers/slip.js ที่ forward ไป cu-central-api ผ่าน END_POINT_CENTRALIZE


Hop 9 · centralize bridge

vtrc-api/api/src/centralize/controllers/graphql.js:10-69GraphqlCDWithToken คือตัวกลาง

1. สร้าง GraphQLClient(END_POINT_CENTRALIZE, { headers: Authorization, apiKey })
2. client.request(query, variables)
3. ถ้า TokenExpiredError →
     ค้น Session ใน db2 (vtrc-centralize schema)
     refreshToken ผ่าน centralize-auth /token
     บันทึก tokenCD ใหม่
     retry (max 3 ครั้ง)
4. return result

ข้อสังเกต — retry counter เป็นพารามิเตอร์ตำแหน่งที่ 4 ที่ default เป็น 0 (graphql.js:13) — ถ้าวนลูปเกิน 3 จะ throw ออกมา ไม่มี circuit breaker


Hop 10 · cu-central-api (Node 12 + Apollo)

cu-central-api/main-api/src/index.js — request ผ่าน GraphQL pipeline ฝั่ง cu-central

  • Apollo Server v2 + Sequelize + custom @auth directive
  • อ่าน query string ที่ vtrc-api ส่งมาจาก centralize/graphql/queries.js
  • resolver ในฝั่ง cu-central เรียก Sequelize model ไปยัง MSSQL

caller ของ NestJS centralize-api (port 3000) ยังไม่ยืนยันจาก code ใน workspace นี้ — ดูรายละเอียดใน บท 5.9


Hop 11 · MSSQL query

ฝั่ง cu-central-api ใช้ Sequelize + raw SQL ไปยัง HRMI MSSQL — เชื่อมผ่าน 6 connection pools (cu-central-api/main-api/src/connector.js:8-170)

js
// ตัวอย่างรูปแบบ — ไม่ใช่ query จริง
Sequelize.literal(`SELECT ... FROM dbHRMI_Center.dbo.uv_i3_PaySlipDT WHERE EmpCode = :empCode`)

parameter binding ใช้ Sequelize placeholder ป้องกัน SQL injection


Latency breakdown โดยประมาณ

ไม่มี production monitoring จริง ตัวเลขนี้คือการประเมินจากโครงสร้าง

Hopเวลาโดยประมาณหมายเหตุ
1-3 (Browser + Nginx)50-200 msnetwork RTT
4-5 (context)5-50 msแล้วแต่ Session table size
6 (@auth)20-100 ms2 query Session + RoleAccess
7-8 (resolver + controller)5-20 msJS overhead
9 (centralize bridge)20-100 msnetwork บวก retry
10-11 (cu-central-api + MSSQL)50-300 msแล้วแต่ query complexity

รวม ~150-770 ms ต่อ request ในเคสปกติ — ไม่รวม cache hit ที่ข้าม hop 9-11


เคล็ดลับตอนทำงานจริง

ตอน debug ให้ไล่จากหลังมาหน้า

ถ้า user บอกว่า "ไม่เห็นสลิป" — อย่าเริ่มที่ frontend ให้เริ่มที่ MSSQL ก่อน

1. HRMI query ได้ row ไหม → ไม่ได้ = ปัญหาที่ HRMI
2. cu-central-api ตอบกลับมาไหม → ไม่ตอบ = ปัญหาที่ cu-central / MSSQL
3. vtrc-api ได้ response ไหม → ไม่ได้ = ปัญหาที่ centralize bridge
4. vtrc-api resolver ส่งกลับ frontend ไหม → ไม่ส่ง = ปัญหาที่ resolver
5. frontend render ผิด → ปัญหาที่ frontend

Redis แทบไม่มีบทบาทใน lifecycle

แม้จะมี Redis (vtrc-api/api/src/lib/redis.js:1-20) แต่ call site ส่วนใหญ่ถูก comment ไว้แล้ว (routes.js:10 import แต่ไม่ใช้) — cache hit/miss จึงแทบไม่มีผลในโลกจริง

ดังนั้นตอน debug ไม่ต้องเสียเวลา clear cache Redis ก่อน — แต่ถ้าใช้จริงในบางจุดที่เรายังไม่เจอ ก็ไม่เสียหายที่จะลอง

error code ที่ frontend รับ บอกอะไรเป็นจำนวนมาก

vtrc-web/src/config/apollo.js:69-164errorLink แยกแยะ error code หลายแบบ

Codeความหมายที่เกิด
TOKEN_EXPIREDJWT หมดอายุvtrc-api auth.js
TOKEN_INVALIDJWT ผิดรูปแบบ หรือ session ไม่มีvtrc-api auth.js
SESSION_EXPIREDSession row expirevtrc-api session.js
ACCESS_KEY_INVALIDapiKey ไม่ matchvtrc-api context
INTERNAL_SERVER_ERRORexception ที่ไม่จัดการทุกที่

ถ้า frontend เด้งไปหน้า login ทันที — ดู error code ที่ Apollo ส่งออก


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

ไป บท 2.4 การเชื่อมต่อกับระบบภายนอก