3.2.6 · TypeDefs + resolvers catalog
บทนี้ catalog GraphQL schema (32 SDL files, 2,629 LOC) และ resolvers (28 files, 1,826 LOC) เพื่อให้ navigate codebase ได้ พร้อมชี้ patterns และ inconsistencies ที่สำคัญ
ขนาดรวม
| ส่วน | LOC | ไฟล์ |
|---|---|---|
| TypeDefs (SDL) | 2,629 | 32 |
| Resolvers | 1,826 | 28 |
| รวม schema layer | ~4,500 | 60 |
เล็กกว่า vtrc-api (queries.js ไฟล์เดียว 4,500 LOC) — แต่กระจายไฟล์ ทำให้ navigate ง่ายกว่า
TypeDefs catalog (32 ไฟล์)
| ไฟล์ | LOC | @auth ใช้? | หัวข้อหลัก |
|---|---|---|---|
root.js | 35 | declare directive | Query, Mutation, Subscribtion (typo), SortingBy, YesNo, JSON, Date |
authentication.js | 56 | pwdVerification @auth | AuthResponse, AuthProfileResponse, AuthDomains, AppCredentials |
personal.js | 116 | personalProfile @auth, employeeProfileHistory @auth | PersonalProfile, EmployeeDetail, ProfileHistory |
employee.js | 271 | 3 fields @auth | EmployeeProfile, WorkingProfile, EmployeeFinancial, AccordingProfile, EmployeeAddress, MeisDivision, EmplGroup, EmpLevel, PositionManage/Technical, ImageContent |
family.js | 58 | employeeFamily @auth | EmployeeFamily, FamilyPersonInfo, FamilyRelationship enum |
organization.js | 97 | none | Organization, OrganizationUnit, OrgazationAddress (typo) |
userVerification.js | 102 | none | requestVerificationCode, profileVerificationChecker, profileVerification |
registration.js | 126 | changePassword @auth | createAccountByVerifyCode, changePasswordByVerifyCode, requestOtp, createSSONotifyUser mutations |
timeAttendance.js | 33 | timeAttendanceByDate @auth | TimeAttendance, TimeAttendanceResponse |
payrollSlip.js | 86 | 2 fields @auth | payrollSlipDetail, payrollSlipDetail6Month |
payrollSlip_removed.js | 112 | — | DEAD FILE — ไม่ถูก import |
finance.js | 26 | employeeFinanceType @auth | EmployeeFinanace (typo), EmployeeBookBank |
bank.js | 40 | none | Bank, BankBranch, BankBook |
tax.js | 106 | none | EmployeeTax (90+ fields, full Thai tax form) |
fund.js | 86 | commented #@auth | ProvidentFund, FundPolicy |
compensation.js | 52 | employeeCompensation @auth | EmployeeCompensation, EmployeeCompensationIncomeDeduct |
referenceData.js | 36 | none | refOrganizations, refOrganizationBelongs, refNewOrganizationUnits |
masterData.js | 86 | none | masterOrgSystem, masterPlace |
searchProfile.js | 44 | searchUser @auth(accessLevel: 4, ignoreAuth: true) | SearchUserResponse |
hrEnum.js | 25 | none | hrEnum query |
notification.js | 25 | none | sendSMSNotification mutation |
hrmi.js | 394 | none | largest SDL — leave/medical/approver domain |
childTuition.js | 88 | none | fetchListChildTuitions, fetchChildTuitionDetail |
salaryCert.js | 59 | none | salaryCertDetail, salaryCert |
hrPerProbation.js | 15 | none | checkHrPerProbationNotApprove |
twoWay.js | 65 | none | 6 queries for 2-way sync |
sso.js | 22 | none | fetchNonEmpSSO, nonEmpSSO |
studyLeaveRequest.js | 105 | none | getEmpStudyLeaveRequestCD, getPersonalCD |
statLog.js | 26 | none | statLog, statLogList |
hr/hrTaxes.js | 113 | hrTaxes @auth | HrTaxesAttr input (~80 fields) |
hr/hrAddress.js | 39 | hrAddress @auth | HrAddressAttr input |
hr/hrPhoneNumber.js | 23 | hrPhoneNumber @auth | HrPhoneNumberAttr input |
hrmi.js — SDL ใหญ่สุด (394 LOC)
ไฟล์เดียวที่ cover leave + medical + approver domain — port มาจาก HRMI leave-management system เก่า
หัวข้อในไฟล์
hrmi.js (394 LOC)
├── Leave
│ ├── getLeaveType
│ ├── getLeaveDocumentList
│ ├── getLeaveDocumentDetail
│ ├── getLeaveDay / getLeaveDayAll
│ ├── getWorkingDay
│ ├── getLeaveShiftTime / getShiftTime
│ ├── createLeaveDocument
│ ├── editLeaveDocument
│ └── deleteLeaveDocument
├── Medical
│ └── getMedical
├── Approver
│ ├── getApprover
│ └── getLeaveApprover
├── Attachment
│ ├── createAttachmentFile
│ ├── updateAttachmentFile
│ └── deleteAttachmentFile
├── Org queries
│ ├── fetchEmpWithOrgnize
│ ├── fetchEmpwithCoA
│ └── employeeDetailProfile
└── enums
├── ApproveStatus
├── DurationType
└── ApproverType extend type Query {
getMedical(
empCode: String!
): [MedicalExpensesLicensee]
getApprover (
docType: ApproverType!
empCode: String!
): [Approver]
getLeaveApprover (
docType: ApproverType!
empCode: String!
leaveTypeId: ID!
leaveDay: Float!
): [Approver]
getLeaveType(
empCode: String
isReqByEmp: Boolean!
): [LeaveTypeData]
getLeaveDocumentList(
empCode: String!
leaveStartDate: Date!
leaveEndDate: Date!
leaveTypeId: ID
approveStatus: ApproveStatus!
): [LeaveDocumentListData]
// ... 24 queries totalแม้ว่า SDL เขียน empCode เป็น parameter แต่ resolver ภายในจะ lookup sourceDb ผ่าน getEmpIdentyCard(empCode) แล้ว pass ไปที่ lib — caller จึงไม่ต้องส่ง sourceDb โดยตรงใน hrmi.js queries (ต่างจาก SDL signature เก่าที่เคยมี sourceDb parameter)
Resolvers catalog (28 ไฟล์)
| ไฟล์ | LOC | Resolvers หลัก |
|---|---|---|
main.js | 14 | Query.__type / __schema → throw (introspection guard) |
authentication.js | 72 | pwdVerification, signInDomains (hardcoded 26-row org list) |
personal.js | 79 | personalProfile, employeeDetail, employeeProfileHistory |
employee.js | 127 | employeeProfile, accordingProfile, employeeFinancial, employeeManagement |
organization.js | 42 | refOrganizations, refOrganizationBelongs |
userVerification.js | 74 | requestVerificationCode, profileVerificationChecker, profileVerification |
registration.js | 386 | isAccountExists, createAccountByVerifyCode, changePassword, requestOtp, createSSONotifyUser |
timeAttendance.js | 33 | timeAttendanceByDate |
payrollSlip.js | 240 | payrollSlipDetail, payrollSlipDetail6Month |
finanace.js | 26 | employeeFinanceType (typo) |
compensation.js | 29 | employeeCompensation |
fund.js | 23 | fetchEmployeeProvidentFund, createChangeBenefitPerson |
masterData.js | 31 | masterOrgSystem, masterOrgSystemCreate, masterOrgSystemUpdate |
searchProfile.js | 23 | searchUser (apiKey-only ผ่าน ignoreAuth) |
hrEnum.js | 17 | hrEnum |
notification.js | 9 | sendSMSNotification |
hrmi.js | 244 | 24 resolvers — leave/medical/approver/attachment |
bank/book.js | 38 | bankBookEmployee, bankBookEmployeeArray |
childTuition.js | 20 | 4 child tuition queries |
salaryCert.js | 12 | salaryCertDetail |
hrPerProbation.js | 12 | checkHrPerProbationNotApprove |
twoWay.js | 37 | 6 queries for 2-way sync |
sso.js | 12 | fetchNonEmpSSO |
studyLeaveRequest.js | 18 | getEmpStudyLeaveRequestCD, getPersonalCD |
statLog.js | 12 | statLog |
hr/hrTaxes.js | 33 | hrTaxes mutation (wraps dbHrTemp.transaction()) |
hr/hrAddress.js | 54 | hrAddress mutation (OTP ถูกปิด) |
hr/hrPhoneNumber.js | 52 | hrPhoneNumber mutation (OTP ยังทำงาน) |
OTP inconsistency (Critical)
hr/hrAddress.js — OTP ปิดไปตั้งแต่ 8 Jul 2021
export default {
Mutation: {
hrAddress: async (obj, { address, otp }, context, info) => {
const { identityCard } = context.userData
const profileKey = address.profileKey
const matched = await personAndEmployeeIsMatch(identityCard, profileKey)
if (matched !== null) {
/*
// ## REMOVE OTP 08Jul2021 ##
// Check OTP is correct
const checkOtpIsCorrect = await checkOtp(otp.otpToken, otp.otpToken, otp.refCode)
if (!checkOtpIsCorrect) {
responseError('', {}, 'OTP_INCORRECT')
}
const t = await Models.db.transaction()
const result = await removeOtp(t, otp.otpToken)
if (!result) {
// Remove OTP error
t.rollback()
log.error(log.logTextFormat('REMOVE_OTP_FAILED', 'mutation:hrAddress', 'ERROR', 'Remove OTP failed (see more at error log).', { profileKey, otpToken: otp.otpToken }))
responseError('', {}, 'INTERNAL_SERVER_ERROR')
}
*/
address.identityCard = identityCard
address.empId = matched.empId
address.sourceDb = matched.sourceDb
const t2 = await Models.dbHrTemp.transaction()
const saved = await saveAddressData(t2, address)
// ...🔴 High (CORR-CU-02) — address mutation (ที่ include email + phone fields ตาม HrAddressAttr input) ไม่ต้องผ่าน OTP
hr/hrPhoneNumber.js — OTP ยังทำงาน
export default {
Mutation: {
hrPhoneNumber: async (obj, { phoneNumber, otp }, context, info) => {
const { identityCard } = context.userData
const profileKey = phoneNumber.profileKey
const matched = await personAndEmployeeIsMatch(identityCard, profileKey)
if (matched !== null) {
// Check OTP is correct
const checkOtpIsCorrect = await checkOtp(otp.otpToken, otp.otpCode, otp.refCode)
if (!checkOtpIsCorrect) {
responseError('', {}, 'OTP_INCORRECT')
}
const t = await Models.db.transaction()
const result = await removeOtp(t, otp.otpToken)
if (!result) {
t.rollback()
log.error(log.logTextFormat('REMOVE_OTP_FAILED', 'mutation:hrPhoneNumber', 'ERROR', 'Remove OTP failed (see more at error log).', { profileKey, otpToken: otp.otpToken }))
responseError('', {}, 'INTERNAL_SERVER_ERROR')
}
phoneNumber.identityCard = identityCard
phoneNumber.empId = matched.empId
phoneNumber.sourceDb = matched.sourceDb
// ...Inconsistency
sibling resolvers 2 ตัว (address + phone) ใช้ pattern ต่างกัน — address แก้ได้ไม่ต้อง OTP แต่ phone ต้อง ควรมี single source of truth สำหรับ "field ไหนบ้างที่ต้อง OTP"
signInDomains — hardcoded org list
authentication.js resolver signInDomains คืน list ของ org ที่ login ได้ โดย hardcoded 26 rows ใน code
const getOrgUnitLevel1 = () => {
return [
{ id: '3F3BF3AD-B4C9-4D44-A56F-AB55C4E4FB01-00_d6873c51-63e0-44a8-9dbd-ccc7d28d2983', value: '...', prefix: '@', textTh: 'กลุ่มงานกลยุทธ์องค์กร', textEn: 'กลุ่มงานกลยุทธ์องค์กร' },
{ id: '3F3BF3AD-...A987969A-...', value: '...', prefix: '@', textTh: 'มูลนิธิสงเคราะห์เด็กของสภากาชาดไทย', textEn: 'Thai Red Cross Children Home' },
{ id: '3F3BF3AD-...CU-RE-STRUCTURE-ID-02', value: '...', prefix: '@', textTh: 'โรงพยาบาลจุฬาลงกรณ์', textEn: 'Chulalongkorn Hospital' },
// ... 23 more
{ id: '3F3BF3AD-...9625B335-...', value: '...', prefix: '@', textTh: 'สำนักงานอาสากาชาด', textEn: 'Red Cross Volunteer Bureau' }
]
}
export default {
Query: {
signInDomains: async (obj, args, context, info) => {
return getOrgUnitLevel1()
// Return Organization DO NOT RETURN SOURCE_DB.
const orgList = await getOrganizations({ order: [['OrgName', 'ASC']] }) // ← dead code หลัง return
// ...
}
}
}🔴 High (QUAL-CU-03) — org list ไม่ได้มาจาก DB ทำให้เพิ่ม/แก้ org ต้อง deploy code ใหม่ ควรย้ายไป masterData table
สังเกต — code หลัง return getOrgUnitLevel1() (บรรทัดที่ 56 เป็นต้นไป) เป็น dead code ที่จะไม่ execute เพราะ return ก่อน
registration.js — ไฟล์ใหญ่สุด (386 LOC)
cover account creation + password management flow
registration.js (386 LOC)
├── isAccountExists (check ก่อน create)
├── createAccountByVerifyCode
├── changePasswordByVerifyCode
├── changePassword
├── requestOtp
├── getVerifyCodeByEmail
└── createSSONotifyUserflow ทั้งหมดผูกกับ SSO + AD + SMS gateway — เป็นไฟล์ที่ซับซ้อนที่สุดใน resolver layer
changeUserPassword helper
const changeUserPassword = async (username, newPassword) => {
const result = { changed: false, error: null }
// 1. Search user from AD for dn
const found = await searchUserFromAd(username)
if (found !== null) {
if (found.count === 1) {
const profile = found.profile[0]
const dn = profile.dn
// 2. Call update password in AD
const update = await adChangeEntryValue(dn, {
unicodePwd: Buffer.from(`"${newPassword}"`, 'utf16le').toString()
}, 'replace')
// ...AD password update — password ถูก wrap ใน double-quote แล้ว encode เป็n UTF-16LE ตาม requirement ของ AD unicodePwd attribute
payrollSlip.js — 240 LOC
resolver ที่คำนวณ payroll slip detail
export default {
PayrollSlip: {
bookNo(parent) {
return !isEmpty(parent.bookNo) ? sensorBookNo(parent.bookNo) : ''
}
},
Query: {
payrollSlipDetail: async (obj, { profileKey, year, month }, context, info) => {
const d = makeDate()
if (month < 1 || month > 12) {
responseError('', { month }, 'MONTH_INVALID')
}
if (year > +d.yyyy) {
responseError('', { month }, 'YEAR_INVALID')
}
const { userData } = context
const profile = await personAndEmployeeIsMatch(userData.identityCard, profileKey)
if (profile !== null) {
const empId = profile.empId
const sourceDb = profile.sourceDb
let payrollSlip = await getPayrollSlipDetail(empId, year, month, sourceDb)
// ...
// If is CU_SC then remove KCMH from division
if (workingProfile.sourceDb === 'dbHRMI_CU_SC_rep') {
if (typeof divisions[0] !== 'undefined') {
if (divisions[0].id === 'CU-RE-STRUCTURE-ID-02') {
// Remove first array index
divisions.splice(0, 1)
}
}
}ดึงข้อมูลจาก DB view uvi3PaySlipHD + uvi3PaySlipDT (models/hrmi/salary/) — เป็น view ที่ MSSQL side define ไว้
Special-case dbHRMI_CU_SC_rep
มี special-case logic สำหรับ shard dbHRMI_CU_SC_rep — ลบ KCMH division ออกจากผลลัพธ์ เป็น business rule เฉพาะที่บ่งบอกว่ามี org structure แตกต่างกันในแต่ละ shard
hrmi.js resolver — 244 LOC
export default {
Query: {
getMedical: async (root, { empCode }, context, info) => {
const empIdenty = await hrmiMedical.getEmpIdentyCard(empCode)
if (empIdenty.length > 0) {
const orgCode = empIdenty[0].orgCode
const identityCard = empIdenty[0].identityCard
return await hrmiMedical.getMedical(identityCard, orgCode, empCode)
} else {
responseError('', { error: 'Employee not found.' }, 'PROFILE_NOT_FOUND')
}
},
getApprover: async (root, { docType, empCode }, context, info) => {
const empIdenty = await hrmiMedical.getEmpIdentyCard(empCode)
// ...pattern ที่ซ้ำในทุก query — getEmpIdentyCard(empCode) เพื่อ resolve identityCard + orgCode (ข้าม shard lookup) แล้ว pass ไป lib
มี debug print หลายจุดที่ลืมลบ (console.log("empIdenty ->", empIdenty) ที่บรรทัด 45, 76, 94, ...)
hrTaxes resolver — transaction pattern
hrTaxes: async (obj, { taxes }, context, info) => {
const { identityCard } = context.userData
const profileKey = taxes.profileKey
const matched = await personAndEmployeeIsMatch(identityCard, profileKey)
if (matched !== null) {
taxes.identityCard = identityCard
taxes.empId = matched.empId
taxes.sourceDb = matched.sourceDb
const t = await Models.dbHrTemp.transaction()
const saved = await saveTaxesData(t, taxes)
if (saved) {
t.commit()
return saved
} else {
t.rollback()
responseError('', {}, 'INTERNAL_SERVER_ERROR')
}
} else {
// Profile doesn't match
responseError('', { profileKey }, 'EMPLOYEE_NOT_FOUND')
}
}pattern mutation ที่ใช้ transaction — t.commit() สำเร็จ หรือ t.rollback() ถ้า fail เป็น pattern เดียวกับ hrAddress และ hrPhoneNumber
userVerification.js — flow 3 ขั้น
requestVerificationCode: async (obj, { requestFor, sendMethod, citizenId, employeeCode, birthDate, org }, context, info) => {
const orgFormat = formatOrgId(org)
org = orgFormat.org
const orgUnit = orgFormat.orgUnit
const profiles = await getEmployeeByEmpCode(employeeCode)
const isProfileAllowed = await checkEmployeeAbleLogin(profiles)
if (isProfileAllowed) {
const workProfileSourceDb = profiles[0].workingProfile.sourceDb
const verified = await userVerification(null, citizenId, employeeCode, birthDate, workProfileSourceDb, true)
if (verified) {
citizenId = cleansingIdentityCardNumber(citizenId)
const workflow = await getAccountWorkflow(citizenId, employeeCode)
if (requestFor === 'CREATE_ACCOUNT' && workflow === 'A') {
responseError('', {}, 'OPERATION_NOT_ALLOWED')
}
return sendVerifyCode(sendMethod, citizenId, employeeCode)
}
}
},flow ที่ซับซ้อน — formatOrgId → getEmployeeByEmpCode → checkEmployeeAbleLogin → userVerification → getAccountWorkflow → sendVerifyCode เป็น 6 ขั้นที่ซ้อนกัน
Patterns ที่สังเกตได้
1. Resolver ไม่ได้แยกตาม bounded context
ไฟล์ resolver ผสมหลาย domain — hrmi.js เดียว cover leave + medical + approver + attachment + org queries (244 LOC) ไม่ได้แยกเป็น leave.js, medical.js, approver.js ตาม domain
ใน Go target จะแยกเป็น bounded context ชัดเจน
2. ไม่มี DataLoader pattern
ทุก query ที่ resolve nested field ทำ N+1 — เช่น Employee ที่มี workingProfile association ถ้า query 100 employee พร้อมกัน จะมี 100+1 query
ใน Go target ใช้ dataloader pattern หรือ join โดยตรง
3. sourceDb กระจายทั้ง schema
parameter sourceDb อยู่ในเกือบทุก query ของ hrmi.js (เก่า) — caller (vtrc-api) ต้องรู้ว่าต้อง pass อะไร
ในเวอร์ชันปัจจุบัน resolver เก็บ logic การ lookup sourceDb ภายใน ผ่าน getEmpIdentyCard(empCode) — caller ส่งแค่ empCode
4. Debug print ลืมลบ
หลาย resolver มี console.log("xxx ->", xxx) ที่ลืมลบ — เป็น evidence ว่าไม่มี lint rule ที่ catch
5. Hardcoded data ใน resolver
signInDomains hardcoded 26 rows, getOrgUnitLevel1 hardcoded org unit list — ควรย้ายไป DB
Dead SDL — payrollSlip_removed.js
112 LOC ที่ไม่ถูก import — เป็นเวอร์ชันเก่าที่ใช้ empCode แทน profileKey
payrollSlipInfo(empCode: String, year: Int, month: Int): Payrolllive version (payrollSlip.js) ใช้ profileKey แทน
payrollSlipDetail(profileKey: String, ...): PayrollSlipDetailmigration จาก empCode → profileKey เกิดขึ้นเพื่อ support multi-tenant (เพราะ empCode ไม่ unique ข้าม shard)
สรุป
| Pattern | สถานะ | Severity |
|---|---|---|
| Schema size | เล็กกว่า vtrc-api | 🟢 Good |
| File organization | ผสม domain ในไฟล์เดียว (hrmi.js) | 🟡 High |
@auth coverage | ไม่ครบทุก field — หลาย field public | 🟡 High |
| OTP consistency | hrAddress vs hrPhoneNumber ต่างกัน | 🔴 Critical |
| Hardcoded data | signInDomains 26 rows | 🟡 High |
| Dead SDL | payrollSlip_removed.js | 🟢 Medium |
| N+1 patterns | ไม่มี DataLoader | 🟡 High |
| Typos in schema | EmployeeFinanace, OrgazationAddress, Subscribtion | 🟢 Medium |
| Debug prints ลืมลบ | หลายจุดใน hrmi.js resolver | 🟢 Medium |
Pattern miss HrTimeAttendance | ไม่มี scope constraint | 🔴 Critical |
อ่านต่อ → 3.2.7 External integrations