11.9 · Forward/proxy pattern (vtrc-api → cu-central-api)
บทนี้อธิบายว่า vtrc-api forward request ไป cu-central-api อย่างไร — pattern, retry, refresh token, และ catalog ของ queries/mutations ที่ส่งไป
ภาพรวม
vtrc-api ไม่ได้เก็บข้อมูล HR master เอง — แต่ forward ไป cu-central-api ผ่าน GraphQL
Frontend
↓ GraphQL
vtrc-api (Apollo)
↓ GraphqlCDWithToken (graphql-request)
cu-central-api (Apollo)
↓ Sequelize
MSSQL shardsทุก call ผ่านไฟล์เดียว: centralize/controllers/graphql.js
1. ที่อยู่ของ forward logic
vtrc-api/api/src/
├── centralize/
│ ├── controllers/
│ │ ├── graphql.js ← forward engine (GraphqlCDWithToken / GraphqlCDWithOutToken)
│ │ ├── auth.js ← REST auth forward (/signin, /token, /signout)
│ │ ├── hrmi.js ← HRMI domain forward
│ │ ├── user.js ← user domain forward
│ │ ├── profile.js
│ │ ├── slip.js
│ │ ├── tax.js
│ │ ├── fund.js
│ │ ├── organization.js
│ │ ├── ... (16 controllers รวม)
│ └── graphql/
│ ├── queries.js ← 68 query templates (49 KB)
│ └── mutations.js ← 17 mutation templates2. GraphqlCDWithToken — forward หลัก
export const GraphqlCDWithToken = async (
query,
token,
variables,
sessionId = null,
i = 0,
apiKey = null
) => {
// สร้าง GraphQLClient ต่อ END_POINT_CENTRALIZE
const client = new GraphQLClient(END_POINT_CENTRALIZE, {
headers: {
Authorization: `Bearer ${token}`,
apiKey: apiKey || API_CENTRALIZE_KEY,
},
})
try {
const data = await client.request(query, variables)
return data
} catch (err) {
if (err instanceof TokenExpiredError && i < 3) {
// token หมดอายุ → refresh + retry (สูงสุด 3 ครั้ง)
const newToken = await refreshToken(sessionId)
return GraphqlCDWithToken(query, newToken, variables, sessionId, i + 1)
}
if (err.response?.errors?.[0]?.extensions?.code === 'UNAUTHENTICATED') {
throw responError('TOKEN_INVALID')
}
throw err
}
}flow การทำงาน
1. Resolver เรียก GraphqlCDWithToken(query, token, vars, sessionId)
↓
2. GraphQLClient.request(query, vars) → ยิงไป cu-central-api
↓
3. ถ้าสำเร็จ → ส่ง data กลับ
↓
4. ถ้า TokenExpiredError และ i < 3:
a. refreshToken(sessionId) → ขอ token ใหม่จาก cu-central-api
b. retry GraphqlCDWithToken(query, newToken, vars, sessionId, i+1)
↓
5. ถ้า UNAUTHENTICATED → throw responError('TOKEN_INVALID')
→ frontend ได้ TOKEN_INVALID → logout + login
↓
6. ถ้า error อื่น → ส่งต่อให้ resolver จัดการข้อระวัง
- retry สูงสุด 3 ครั้ง — ถ้า refresh token ก็หมดอายุ → ล้มเหลวหลัง retry 3
- ไม่มี circuit breaker — ถ้า cu-central-api ล่ม vtrc-api ยังพยายามยิงต่อ
- ไม่มี timeout — ถ้า cu-central-api ช้า vtrc-api รอเรื่อย ๆ
3. GraphqlCDWithOutToken — public queries
export const GraphqlCDWithOutToken = async (query, variables) => {
const client = new GraphQLClient(END_POINT_CENTRALIZE, {
headers: {
apiKey: API_CENTRALIZE_KEY,
},
})
const data = await client.request(query, variables)
return data
}ใช้สำหรับ query ที่ไม่ต้อง login:
signInDomains(รายการ domain สำหรับ login)
4. REST auth forward — centralize/controllers/auth.js
cu-central-api มี REST endpoint สำหรับ auth — vtrc-api เรียกผ่าน axios:
POST /auth/signin
export const signInCD = async (username, password, org, apiKey) => {
const response = await axios({
method: 'POST',
url: `${END_POINT_CENTRALIZE_AUTH}/signin`,
data: qs.stringify({ username, password, org, apiKey }),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
return response.data
}POST /auth/signinbypass
// เหมือนข้างบนแต่ endpoint คือ /signinbypassPOST /auth/signout
export const signOutCD = async (accessToken, apiKey) => {
const response = await axios({
method: 'POST',
url: `${END_POINT_CENTRALIZE_AUTH}/signout`,
data: qs.stringify({ accessToken, apiKey }),
})
return response.data
}POST /auth/token (refresh)
export const refreshToken = async (refreshTokenCD, apiKey) => {
const response = await axios({
method: 'POST',
url: `${END_POINT_CENTRALIZE_AUTH}/token`,
data: qs.stringify({
refreshToken: refreshTokenCD,
apiKey,
}),
})
return response.data
}5. Query/Mutation catalog — cu-central-api
vtrc-api เก็บ query template ไว้ใน 2 ไฟล์:
centralize/graphql/queries.js — 68 queries
ตัวอย่าง:
// ตัวอย่าง concept
export const fetchEmployeeProfile = gql`
query FetchEmployeeProfile($profileKey: String!) {
employeeProfile(profileKey: $profileKey) {
empCode
firstName
lastName
# ...
}
}
`
export const fetchPayrollSlipDetail = gql`
query FetchPayrollSlipDetail($profileKey: String!, $year: Int!, $month: Int!) {
payrollSlipDetail(profileKey: $profileKey, year: $year, month: $month) {
# ...
}
}
`centralize/graphql/mutations.js — 17 mutations
ตัวอย่าง:
export const createLeaveDocument = gql`
mutation CreateLeaveDocument($input: LeaveDocumentInput!) {
createLeaveDocument(input: $input) {
id
documentNo
}
}
`6. 16 controllers ที่ใช้ forward
ทุก controller ใน centralize/controllers/ ใช้ GraphqlCDWith*:
| Controller | Domain | ใช้บ่อย |
|---|---|---|
auth.js | login, refresh, signout | ทุก login |
hrmi.js | leave, medical | ทุกการลา |
user.js | profile, account | ทุกครั้งที่ดึง profile |
profile.js | personal data | หน้า profile |
slip.js | payslip | หน้าสลิป |
tax.js | tax | หน้าภาษี |
fund.js | provident fund | หน้ากองทุน |
organization.js | org tree | เมนูมือถือ |
masterData.js | master | dropdown |
childTuition.js | child tuition | สวัสดิการ |
salaryCert.js | salary cert | ใบรับรองเงินเดือน |
studyLeaveRequest.js | study leave | ลาศึกษา |
timeAttendance.js | attendance | ลงเวลา |
address.js | address | profile |
statLog.js | stat log | internal |
verification.js | verification | account |
graphql.js | forward engine | (helper) |
7. End-to-end flow — ตัวอย่าง "ดึง profile"
[1] Frontend ส่ง:
POST /api/graphql
query { fetchProfile { id firstName lastName } }
Headers: Authorization: Bearer <jwt-vtrc-api>, apiKey: <web>
↓
[2] vtrc-api Apollo:
- ตรวจ apiKey → OK
- @auth directive → ตรวจ JWT vtrc-api → OK
- session ใน DB → OK
- เรียก resolver fetchProfile
↓
[3] resolvers/profile.js:
- เรียก lib/controllers/profile.js
↓
[4] lib/controllers/profile.js:
- ดึง cu-central-api token จาก Session row
- เรียก GraphqlCDWithToken(
centralizeQueries.fetchEmployeeProfile,
cuToken,
{ profileKey },
sessionId
)
↓
[5] GraphqlCDWithToken:
- สร้าง GraphQLClient
- ส่ง query ไป END_POINT_CENTRALIZE (CLIENT_TARGET ตัวอย่าง http://localhost:8082/api/graphql)
- Headers: Authorization: Bearer <cu-jwt>, apiKey: <cu-key>
↓
[6] cu-central-api Apollo:
- ตรวจ apiKey → OK
- ตรวจ JWT cu-central-api → OK
- ตรวจ session ใน Redis → OK
- เรียก resolver employeeProfile
↓
[7] cu-central-api resolver:
- splitProfileKey(profileKey) → { empId, sourceDb }
- เรียก lib/employeeProfile.js
- ส่ง sourceDb เป็น where clause ใน MSSQL
↓
[8] MSSQL → ส่งข้อมูลกลับ
↓
[9] cu-central-api → ส่งกลับ vtrc-api (ผ่าน GraphQLClient)
↓
[10] vtrc-api resolver → แปลงข้อมูล (บางครั้ง) → ส่งกลับ frontend8. JWT 2 ตัว
เพราะมี 2 services แต่ละตัวมี JWT ของตัวเอง:
| JWT | ใคร issue | ใครถือ | ใช้ยิงไหน |
|---|---|---|---|
| JWT vtrc-api | vtrc-api | frontend | ยิง vtrc-api |
| JWT cu-central-api | cu-central-api | vtrc-api (เก็บใน Session table) | ยิง cu-central-api |
frontend ไม่เคยเห็น JWT cu-central-api — เป็น implementation detail ของ vtrc-api
9. Refresh token flow
[1] vtrc-api ยิง cu-central-api ด้วย JWT cu-central-api
↓
[2] cu-central-api ตอบ: TokenExpiredError
↓
[3] vtrc-api GraphqlCDWithToken จับ error:
- ดึง refreshTokenCD จาก Session table
- เรียก refreshToken() → POST /auth/token ที่ cu-central-api
↓
[4] cu-central-api:
- ตรวจ refreshToken ใน Redis
- issue JWT cu-central-api ใหม่
- ส่งกลับ vtrc-api
↓
[5] vtrc-api:
- อัปเดต Session row ด้วย JWT cu-central-api ใหม่
- retry query เดิมด้วย token ใหม่ (i+1)
↓
[6] ถ้าสำเร็จ → ส่งข้อมูลกลับ frontend
ถ้า fail อีก → retry จนกว่า i=3 → throw error10. ข้อระวังทั้งหมด
1. Retry 3 ครั้งอาจไม่พอ
- ถ้า cu-central-api มีปัญหาจริง ๆ retry 3 ครั้งก็ fail
- user ได้ error หลังรอนาน
2. ไม่มี circuit breaker
- ถ้า cu-central-api ล่ม vtrc-api ยังพยายามยิง
- เปลือง resource + ทำให้ vtrc-api ช้าลง
3. ไม่มี timeout
graphql-requestdefault ไม่มี timeout- ถ้า cu-central-api ค้าง → vtrc-api ค้างตาม
4. Error mapping ไม่สมบูรณ์
- cu-central-api ส่ง
UNAUTHENTICATED→ vtrc-api แปลงเป็นTOKEN_INVALID - แต่ error code อื่น ๆ ของ cu-central-api อาจไม่ถูก map → frontend สับสน
5. Session table เป็นจุดศูนย์กลาง
- vtrc-api เก็บ JWT cu-central-api ใน MariaDB Session table
- ถ้า DB ล่ม → vtrc-api ไม่สามารถ refresh ได้
6. Race condition ตอน refresh
- ถ้ามี request 3 ตัวยิงเข้ามาพร้อมกัน และทั้ง 3 ได้ TokenExpiredError
- แต่ละตัวเรียก refreshToken เอง → 3 request ไป cu-central-api
- อาจทำให้ token เก่า invalid ก่อนที่ request อื่นจะใช้
7. hard-coded apiKey
// centralize/controllers/graphql.js
const client = new GraphQLClient(END_POINT_CENTRALIZE, {
headers: {
apiKey: apiKey || API_CENTRALIZE_KEY, // ค่า default จาก config
},
})API_CENTRALIZE_KEY อยู่ใน .env — เป็น secret ของ cu-central-api
11. การปรับปรุงที่แนะนำ
ระยะสั้น
- เพิ่ม timeout ใน GraphQLClient (เช่น 30 วินาที)
- เพิ่ม circuit breaker — ถ้า cu-central-api fail 5 ครั้งติด → short-circuit 60 วินาที
- sync refresh — ทำให้ refresh เป็น singleton promise (ถ้ากำลัง refresh อยู่ request อื่นรอ)
ระยะกลาง
- log retry — เก็บ stat ว่าเกิด retry บ่อยแค่ไหน
- degrade gracefully — ถ้า cu-central-api ล่ม vtrc-api ควรมี cache หรือ default บางอย่าง
- observation — track latency ของแต่ละ forward call
เปรียบเทียบกับ direct call
| ด้าน | ผ่าน vtrc-api | ยิง cu-central-api ตรง ๆ |
|---|---|---|
| Frontend simplicity | ง่าย (endpoint เดียว) | ซับซ้อน (2 endpoint) |
| Auth | JWT vtrc-api อย่างเดียว | ต้อง JWT ทั้งคู่ |
| Latency | +50-200 ms (forward overhead) | ตรง |
| SourceDB | vtrc-api จัดการให้ | frontend ต้องรู้เอง |
| Security | vtrc-api เป็น trust boundary | frontend ต้องไว้ใจ cu-central-api |
| Maintainability | เปลี่ยน cu-central-api ได้โดยไม่กระทบ frontend | เปลี่ยนกระทบทุก client |
การออกแบบปัจจุบัน (forward) มีเหตุผล — เพราะ vtrc-api เป็น gateway ที่เพิ่ม business logic + ซ่อนความซับซ้อนของ cu-central-api