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 ไม่ใช่ NestJScentralize-api(port 3000, REST) — แยกพอร์ต: CODE_DEFAULTAPP_PORT=8080, CLIENT_TARGET ใน.env.exampleอาจเป็น8082ดู บท 3.4
รายละเอียดแต่ละ hop
Hop 1-2 · Frontend
vtrc-web/src/config/apollo.js:48-67 — authLink แปะ headers ทุก request
// ตัวอย่างสรุป ไม่ใช่โค้ดจริงทั้งหมด
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 อย่างเท่านั้น
- TLS termination (
*:443→ HTTP) - 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 bridgeresolver เป็น 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-69 — GraphqlCDWithToken คือตัวกลาง
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)
// ตัวอย่างรูปแบบ — ไม่ใช่ 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 ms | network RTT |
| 4-5 (context) | 5-50 ms | แล้วแต่ Session table size |
| 6 (@auth) | 20-100 ms | 2 query Session + RoleAccess |
| 7-8 (resolver + controller) | 5-20 ms | JS overhead |
| 9 (centralize bridge) | 20-100 ms | network บวก 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 ผิด → ปัญหาที่ frontendRedis แทบไม่มีบทบาทใน 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-164 — errorLink แยกแยะ error code หลายแบบ
| Code | ความหมาย | ที่เกิด |
|---|---|---|
TOKEN_EXPIRED | JWT หมดอายุ | vtrc-api auth.js |
TOKEN_INVALID | JWT ผิดรูปแบบ หรือ session ไม่มี | vtrc-api auth.js |
SESSION_EXPIRED | Session row expire | vtrc-api session.js |
ACCESS_KEY_INVALID | apiKey ไม่ match | vtrc-api context |
INTERNAL_SERVER_ERROR | exception ที่ไม่จัดการ | ทุกที่ |
ถ้า frontend เด้งไปหน้า login ทันที — ดู error code ที่ Apollo ส่งออก