7.2 KiB
Permission Template Validation Fix Report
Executive Summary
แก้ปัญหา validation ของการเลือก Permission Template ในฟอร์มกำหนดสิทธิ์ผู้ใช้ โดยทำให้ชนิดข้อมูลของ template_id สอดคล้องกันตั้งแต่ Select บน frontend, TanStack Form state, Zod validation, API payload, route handler และ database ที่ใช้ integer
Root Cause Analysis
สาเหตุหลักคือ Select component ส่งค่าออกมาเป็น string ตามพฤติกรรมของ Radix Select และ shared FormSelectField ของโปรเจกต์ก็ส่งค่าต่อเข้า form state แบบ string ตรง ๆ แต่ userPermissionAssignmentSchema กำหนด template_id เป็น z.number()
ผลที่เกิดขึ้นคือ
- เมื่อผู้ใช้เลือกค่าใน dropdown, form state กลายเป็น
string - Zod ตรวจ
template_idเป็นnumberจึงขึ้น errorInvalid input: expected number, received string - component ต้องพยายาม
Number(value.template_id)ตอน submit เพื่อแก้ปลายทาง - logic ที่หา template ปัจจุบันในหน้า edit มีโอกาส mismatch เพราะ
template.idเป็นnumberแต่ค่าจาก form อาจเป็นstring
Architecture Review
Flow หลังแก้ไขเป็นดังนี้
Frontend
-> Select ใช้ string เฉพาะใน UI
-> onValueChange แปลงค่าเป็น number ก่อนเก็บใน form state
Validation
-> userPermissionAssignmentSchema รองรับ string ที่หลุดเข้ามาได้ด้วย z.preprocess
-> คืนข้อความผู้ใช้ กรุณาเลือก Permission Template
API
-> saveUserPermissionAssignment() ส่ง template_id เป็น number
Backend
-> route handler ใช้ schema เดิมตรวจ payload
-> service layer รับ template_id: number
Database
-> user_permission_template_assignments.template_id และ permission_templates.id เป็น integer
Files Modified
- src/features/permission-management/components/user-permission-sheet.tsx
ปรับ select ของ
template_idให้แปลงค่าstring -> numberก่อนเก็บลง form state และเลิกแปลงซ้ำตอน submit - src/features/permission-management/schemas/user-permission-assignment.ts
ปรับ schema ให้ preprocess ค่า
template_idและคืนข้อความ validation ที่เป็นมิตรกับผู้ใช้
Implementation Details
Form
- เปลี่ยน default ของ
template_idจาก fallback0เป็นค่าเดิมหรือ template แรกที่มีอยู่จริง - เก็บค่าใน TanStack Form เป็น
number | undefinedแทนstring
Select
- ใช้ custom
form.AppFieldแทน sharedFormSelectFieldเฉพาะจุดนี้ - แปลง
field.state.valueเป็นString(...)เฉพาะตอน bind เข้าSelect - แปลงค่ากลับเป็น
Number(value)ตอนonValueChange - ป้องกันเคส empty value ไม่ให้กลายเป็น
0
Schema
- ใช้
z.preprocessสำหรับtemplate_id - ถ้าเป็น string ว่าง จะเปลี่ยนเป็น
undefined - ถ้าเป็น numeric string จะเปลี่ยนเป็น
number - ถ้าไม่ผ่านเงื่อนไข จะได้ข้อความ
กรุณาเลือก Permission Template
API / Backend
- payload จาก frontend ถูกส่งเป็น
numberแล้ว - route handler และ service layer ไม่ต้องเปลี่ยน business logic
Before
- เลือก Permission Template แล้ว form มีโอกาสถือค่าเป็น
string - schema คาดหวัง
number - ผู้ใช้เห็น error เชิงเทคนิค
Invalid input: expected number, received string - หน้า edit อาจหา template ปัจจุบันไม่ตรงชนิดข้อมูล
After
- ค่า
template_idใน form state และ payload เป็นnumber - schema และ API รับค่าชนิดเดียวกับ database
- ผู้ใช้เห็นข้อความ validation ที่เข้าใจได้
- create และ edit ใช้ logic ชนิดข้อมูลเดียวกันตลอด flow
Validation Result
ตรวจจากโค้ดและ data flow หลังแก้ไขแล้วได้ผลดังนี้
- Create: เลือก template แล้ว submit จะส่ง
template_idเป็นnumber - Edit: ค่าเดิมของ template ถูกโหลดเป็น
numberและ bind กลับเข้า select ได้ถูกต้อง - Validation: หากไม่มีค่า จะได้ข้อความ
กรุณาเลือก Permission Template - API: route handler ยังใช้ schema เดิม แต่รับค่า numeric string ได้อย่างปลอดภัยและ normalize เป็น
number - Database: ไม่มีการเปลี่ยน schema และยังสอดคล้องกับ
integer
Regression Check
- ไม่เปลี่ยน permission logic
- ไม่เปลี่ยน API endpoint
- ไม่เปลี่ยน database schema
- ไม่เพิ่ม library ใหม่
- ไม่เพิ่ม
any - ใช้ shared
Selectcomponent เดิมของระบบ
Risks
- shared
FormSelectFieldของโปรเจกต์ยังเป็น string-based ตามเดิม ดังนั้นถ้ามีฟอร์มอื่นที่ใช้ select กับ schema แบบnumberอาจเจอ pattern เดียวกันได้
Recommendation
- หากอนาคตมีหลายฟอร์มที่ต้องใช้ select กับค่า
number, ควรเพิ่ม shared numeric select wrapper ที่อยู่บน pattern เดิมของtanstack-form - ควรเพิ่ม automated test ระดับ component หรือ integration สำหรับ create/edit flow ของ user permission assignment