Skip to content

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 templates

2. GraphqlCDWithToken — forward หลัก

10:69:centralize/controllers/graphql.js
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

71:104:centralize/controllers/graphql.js
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

27:49:centralize/controllers/auth.js
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

51:72:centralize/controllers/auth.js
// เหมือนข้างบนแต่ endpoint คือ /signinbypass

POST /auth/signout

74:94:centralize/controllers/auth.js
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)

96:135:centralize/controllers/auth.js
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

ตัวอย่าง:

js
// ตัวอย่าง 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

ตัวอย่าง:

js
export const createLeaveDocument = gql`
  mutation CreateLeaveDocument($input: LeaveDocumentInput!) {
    createLeaveDocument(input: $input) {
      id
      documentNo
    }
  }
`

6. 16 controllers ที่ใช้ forward

ทุก controller ใน centralize/controllers/ ใช้ GraphqlCDWith*:

ControllerDomainใช้บ่อย
auth.jslogin, refresh, signoutทุก login
hrmi.jsleave, medicalทุกการลา
user.jsprofile, accountทุกครั้งที่ดึง profile
profile.jspersonal dataหน้า profile
slip.jspayslipหน้าสลิป
tax.jstaxหน้าภาษี
fund.jsprovident fundหน้ากองทุน
organization.jsorg treeเมนูมือถือ
masterData.jsmasterdropdown
childTuition.jschild tuitionสวัสดิการ
salaryCert.jssalary certใบรับรองเงินเดือน
studyLeaveRequest.jsstudy leaveลาศึกษา
timeAttendance.jsattendanceลงเวลา
address.jsaddressprofile
statLog.jsstat loginternal
verification.jsverificationaccount
graphql.jsforward 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 → แปลงข้อมูล (บางครั้ง) → ส่งกลับ frontend

8. JWT 2 ตัว

เพราะมี 2 services แต่ละตัวมี JWT ของตัวเอง:

JWTใคร issueใครถือใช้ยิงไหน
JWT vtrc-apivtrc-apifrontendยิง vtrc-api
JWT cu-central-apicu-central-apivtrc-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 error

10. ข้อระวังทั้งหมด

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-request default ไม่มี 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

js
// 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. การปรับปรุงที่แนะนำ

ระยะสั้น

  1. เพิ่ม timeout ใน GraphQLClient (เช่น 30 วินาที)
  2. เพิ่ม circuit breaker — ถ้า cu-central-api fail 5 ครั้งติด → short-circuit 60 วินาที
  3. sync refresh — ทำให้ refresh เป็น singleton promise (ถ้ากำลัง refresh อยู่ request อื่นรอ)

ระยะกลาง

  1. log retry — เก็บ stat ว่าเกิด retry บ่อยแค่ไหน
  2. degrade gracefully — ถ้า cu-central-api ล่ม vtrc-api ควรมี cache หรือ default บางอย่าง
  3. observation — track latency ของแต่ละ forward call

เปรียบเทียบกับ direct call

ด้านผ่าน vtrc-apiยิง cu-central-api ตรง ๆ
Frontend simplicityง่าย (endpoint เดียว)ซับซ้อน (2 endpoint)
AuthJWT vtrc-api อย่างเดียวต้อง JWT ทั้งคู่
Latency+50-200 ms (forward overhead)ตรง
SourceDBvtrc-api จัดการให้frontend ต้องรู้เอง
Securityvtrc-api เป็น trust boundaryfrontend ต้องไว้ใจ cu-central-api
Maintainabilityเปลี่ยน cu-central-api ได้โดยไม่กระทบ frontendเปลี่ยนกระทบทุก client

การออกแบบปัจจุบัน (forward) มีเหตุผล — เพราะ vtrc-api เป็น gateway ที่เพิ่ม business logic + ซ่อนความซับซ้อนของ cu-central-api


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

ไป บท 10.10 Scorecard + ปัญหา API surface