Hooks
หน้านี้สอนอะไร
การรันโค้ดของคุณเมื่อเอกสารถูกสร้าง แก้ อ่าน หรือลบ พร้อมตัวอย่างสำหรับกรณีที่พบบ่อย
ควรอ่านก่อน: การตั้งค่า
Hook รันโค้ดของคุณในจังหวะต่างๆ ของเอกสาร และทำงานกับทุกช่องทาง: Local API, REST API และหน้า admin แต่ละ hook เป็น list ของฟังก์ชัน plugin จึงเพิ่มของตัวเองต่อจากของคุณได้
{
slug: 'posts',
hooks: {
beforeChange: [({ data }) => ({ ...data, readingTime: minutes(data.body) })],
afterChange: [({ doc }) => notifyTeam(`บันทึกแล้ว: ${doc.title}`)],
},
fields: [/* … */],
}Hook ของ collection
| Hook | ทำงานเมื่อ | อาร์กิวเมนต์ | ค่าที่คืน |
|---|---|---|---|
beforeValidate | ก่อนตรวจ field | data, operation, originalDoc? | data ใหม่ หรือไม่คืน |
beforeChange | หลังตรวจ ก่อนบันทึก | data, operation, originalDoc? | data ใหม่ หรือไม่คืน |
afterChange | หลังบันทึก | doc, operation, previousDoc? | — |
beforeDelete | ก่อนลบ | id | — |
afterDelete | หลังลบ | id, doc | — |
afterRead | ทุกเอกสารที่ส่งกลับ | doc | doc ใหม่ หรือไม่คืน |
ทุก hook ได้รับเพิ่ม:
user: ผู้ที่ทำ หรือnull(ไม่ได้ login หรือเรียก Local API จากโค้ดที่เชื่อถือได้)cms: Local API สำหรับอ่านหรือเขียน collection อื่นslug: slug ของ collection (หรือ global) ใช้สะดวกเมื่อฟังก์ชันเดียวใช้กับหลายที่
operation เป็น 'create' หรือ 'update' ตอน update originalDoc คือเอกสารก่อนแก้ และ data มีเฉพาะส่วนที่กำลังเปลี่ยน
global รองรับ beforeChange, afterChange และ afterRead
ลำดับและ error
beforeValidate → ตรวจสอบ → beforeChange → บันทึก → afterChange
- hook แบบ before ที่ throw จะยกเลิกการทำงาน และ error ส่งถึงผู้เรียก ให้ throw
ValidationErrorหรือForbiddenErrorจาก@easy-cms/coreเพื่อให้หน้า admin แสดงข้อความชัดเจน - hook แบบ after ที่ throw จะถูกบันทึก log และการเปลี่ยนแปลงยังถูกบันทึกอยู่
afterReadทำงานกับทุกเอกสารที่ส่งกลับ รวมถึง relationship ที่ populate ก่อนที่ field ที่ซ่อนและ field ที่ไม่มีสิทธิ์อ่านจะถูกตัดออก- hook ใน list ทำงานทีละตัว แต่ละตัวเห็นผลของตัวก่อนหน้า
- hook ทำงานนอก transaction ของฐานข้อมูล:
afterChangeเห็นเอกสารที่บันทึกแล้ว และ hook ที่ช้าจะทำให้ response ช้าตาม งานที่ใช้เวลานานให้ส่งไป queue หรือ webhook
ตัวอย่าง
ใส่ผู้เขียนตอนสร้าง
beforeChange: [
({ data, operation, user }) =>
operation === 'create' && user ? { ...data, author: user.id } : data,
],อัปเดต field ที่คำนวณจาก field อื่น
import { richTextToPlainText } from '@easy-cms/richtext'
beforeChange: [
({ data }) =>
data.body === undefined
? data // update ครั้งนี้ไม่ได้เปลี่ยน body
: { ...data, readingTime: Math.ceil(richTextToPlainText(data.body).split(/\s+/).length / 200) },
],รีเฟรชหน้าเว็บหลังเผยแพร่ (Next.js)
import { revalidatePath } from 'next/cache'
afterChange: [
({ doc }) => {
revalidatePath('/')
if (doc.slug) revalidatePath(`/posts/${doc.slug}`)
},
],ถ้าเว็บ build อยู่ที่อื่น (เว็บ static บน CDN) ให้ใช้ webhook แทน เพราะส่งซ้ำเมื่ออีกฝั่งล่ม
ไม่ให้ลบสิ่งที่ยังถูกใช้อยู่
import { ForbiddenError } from '@easy-cms/core'
// ใน collection categories
beforeDelete: [
async ({ id, cms }) => {
const totalDocs = await cms.count('posts', { where: { category: { equals: id } } })
if (totalDocs > 0) throw new ForbiddenError(`ยังมี ${totalDocs} บทความใช้หมวดหมู่นี้`)
},
],เพิ่มค่าที่คำนวณให้ผลที่อ่าน
afterRead: [({ doc }) => ({ ...doc, url: `/posts/${doc.slug}` })],ค่าจาก afterRead ถูกส่งให้ client แต่ไม่ถูกเก็บและค้นหาไม่ได้ ถ้าต้องกรองหรือเรียงด้วยค่านี้ ให้เก็บด้วย beforeChange
ข้อควรระวัง
- วนซ้ำไม่จบ: การเรียก
cms.update()กับ collection เดียวกันในafterChangeของมันเองจะทำให้ hook ทำงานอีกรอบ ให้แก้dataในbeforeChangeแทน - สิทธิ์: การเรียก
cmsใน hook ข้ามกฎสิทธิ์ (เป็นโค้ดฝั่ง server ที่เชื่อถือได้) ส่ง{ user, overrideAccess: false }เพื่อทำในนามผู้ใช้ - update บางส่วน: ตอน update
dataมีเฉพาะ field ที่กำลังเปลี่ยน ค่าอื่นให้อ่านจากoriginalDoc
ขั้นต่อไป
- การควบคุมสิทธิ์: กำหนดว่าใครทำอะไรได้ แทนการตรวจใน hook
- Webhooks: แจ้งบริการอื่นเมื่อเนื้อหาเปลี่ยน พร้อมการส่งซ้ำ