Thêm sub-collection hd_* mới
Vị trí mã nguồn
Phần tiêu đề “Vị trí mã nguồn”Thư mụcadmin/packages/helpdesk/app/
- data/helpdesk-schema.ts collections + fields + relations (sort 2→19)
- composables/use-helpdesk-<name>.ts entity store mới
Thư mụcadmin/packages/helpdesk/server/api/helpdesk/<name>/
- index.get.ts · index.post.ts · [id].get.ts · [id].patch.ts · [id].delete.ts
Thư mụcbackend/extensions/helpdesk/src/endpoints/
- controllers/meta.controller.ts ALLOWED (nếu form cần field metadata)
- controllers/crud.factory.ts makeCrud(collection) — factory dùng chung
- controllers/<name>.controller.ts 1 dòng gọi makeCrud(‘hd_<name>’)
- routes.ts registerCrud(router, ctx, ‘/<name>’, Controller)
Bước 1 — Schema: collection + field, đúng thứ tự sort
Phần tiêu đề “Bước 1 — Schema: collection + field, đúng thứ tự sort”Entry đăng ký collection (không field) — ví dụ hd_labels:
{ collection: "hd_labels", meta: { hidden: true, sort: 3, group: "helpdesk", accountability: "all" }, schema: { name: "hd_labels" },},Field khai báo tách rời trong cùng mảng collections (xem Thêm field vào collection để biết shape field), quan hệ (FK/M2M) khai báo trong mảng relations riêng ở cuối file.
FK ordering — sort chạy 2 → 19, phải tôn trọng phụ thuộc:
| sort | collection | Vì sao ở vị trí này |
|---|---|---|
| 2–7 | hd_working_hours, hd_labels, hd_teams, hd_sla_policies, hd_settings, hd_connections |
Bảng lookup/độc lập — không phụ thuộc FK nào |
| 8 | hd_agents |
Phụ thuộc c_contacts (đã tồn tại từ trước — setup chạy trên DB có sẵn contacts) |
| 9 | hd_inboxes |
Phụ thuộc connections, agents, teams, working_hours |
| 10–11 | hd_templates, hd_canned_responses |
Phụ thuộc inboxes/teams |
| 12 | hd_conversations |
Phụ thuộc inboxes, agents, teams, sla_policies, c_contacts |
| 13 | hd_messages |
Phụ thuộc conversations, agents, contacts, templates, chính nó (reply-chain) |
| 14–16 | hd_conversation_labels, hd_inbox_agents, hd_team_agents |
Junction M2M |
| 17 | hd_message_attachments |
Phụ thuộc messages + odp_files |
| 18–19 | hd_automation_rules, hd_saved_views |
Phụ thuộc agents (owner) |
Collection mới thêm vào sau mọi collection nó tham chiếu FK — sai thứ tự này setup wizard sẽ fail vì FK trỏ tới bảng chưa tồn tại.
Bước 2 — Allowlist field-metadata (nếu form cần)
Phần tiêu đề “Bước 2 — Allowlist field-metadata (nếu form cần)”Nếu form của collection mới cần đọc metadata field qua endpoint field-registry, thêm tên collection vào ALLOWED ở backend:
const ALLOWED = new Set<string>([ 'hd_inboxes', 'hd_agents', 'hd_teams', 'hd_sla_policies', 'hd_working_hours', 'hd_automation_rules', 'hd_canned_responses', 'hd_templates', 'hd_saved_views', 'hd_connections', 'hd_settings', 'hd_conversations', 'hd_messages', 'c_contacts', // 'hd_ten_moi', ← thêm dòng này nếu form cần field metadata]);Bỏ qua bước này nếu form mới viết tay hoàn toàn (không cần schema-driven hint).
Bước 3 — Controller: tái dùng crud.factory.ts
Phần tiêu đề “Bước 3 — Controller: tái dùng crud.factory.ts”makeCrud(collection) trả sẵn 5 handler list/get/create/update/remove với pagination cap + MetaService count:
export function makeCrud(collection: string) { // list/get/create/update/remove — readByQuery + MetaService, clamp limit return { list, get, create, update, remove };}Controller cho collection mới chỉ còn 1 dòng (mirror labels.controller.ts):
import { makeCrud } from './crud.factory.js';
export const { list, get, create, update, remove } = makeCrud('hd_<name>');Bước 4 — Route: registerCrud + withAccess guard
Phần tiêu đề “Bước 4 — Route: registerCrud + withAccess guard”function guard(ctx: AppContext, action: string, handler: (ctx: AppContext) => any) { return withAccess(ctx, 'helpdesk', action, handler(ctx));}
function registerCrud( router: any, ctx: AppContext, base: string, controller: { list: (ctx: AppContext) => any; get: (ctx: AppContext) => any; create: (ctx: AppContext) => any; update: (ctx: AppContext) => any; remove: (ctx: AppContext) => any; },): void { router.get(`${base}`, guard(ctx, 'read', controller.list)); router.post(`${base}`, guard(ctx, 'create', controller.create)); router.get(`${base}/:id(\\d+)`, guard(ctx, 'read', controller.get)); router.patch(`${base}/:id(\\d+)`, guard(ctx, 'update', controller.update)); router.delete(`${base}/:id(\\d+)`, guard(ctx, 'delete', controller.remove));}Đăng ký (mirror labels — routes.ts:63-64):
// <Name> CRUDregisterCrud(router, ctx, '/<name>', NameController);Bước 5 — BFF route + entity store phía FE
Phần tiêu đề “Bước 5 — BFF route + entity store phía FE”BFF route (mirror labels/index.get.ts + [id].patch.ts):
import { customEndpoint } from "@odp/sdk"
export default defineEventHandler(async (event) => { assertPermission(event, { module: "helpdesk", action: "read" }) const userApi = event.context.userApi const query = getQuery(event)
const result = await userApi.request(customEndpoint({ path: "/helpdesk/<name>/", method: "GET", params: query, }))
return result})import { customEndpoint } from "@odp/sdk"
export default defineEventHandler(async (event) => { assertPermission(event, { module: "helpdesk", action: "update" }) const userApi = event.context.userApi const id = getRouterParam(event, "id")! const body = await readBody(event)
const result = await userApi.request(customEndpoint({ path: `/helpdesk/<name>/${id}`, method: "PATCH", body: JSON.stringify(body), }))
return { data: result }})Entity store — copy nguyên khuôn use-helpdesk-labels.ts (60 dòng), đổi tên collection/type:
export function useHelpdesk<Name>() { const api = useHelpdeskApi() const items = useState<Name[]>("helpdesk:<name>", () => []) const loaded = useState<boolean>("helpdesk:<name>:loaded", () => false)
async function fetchAll() { const res = await api.list<Name>() items.value = res.data loaded.value = true } async function ensureLoaded() { if (!loaded.value) await fetchAll() } function getById(id: string) { return items.value.find(x => x.id === id) } async function create(input: Omit<Name, "id">) { const created = await api.create<Name>(input as Partial<Name>) items.value = [...items.value, created] return created } async function update(id: string, patch: Partial<Name>) { const updated = await api.update<Name>(id, patch) const idx = items.value.findIndex(x => x.id === id) if (idx !== -1) { const next = [...items.value]; next[idx] = updated; items.value = next } return updated } async function remove(id: string) { await api.remove<Name>(id) items.value = items.value.filter(x => x.id !== id) }
return { items, loaded, fetchAll, ensureLoaded, getById, create, update, remove }}Pattern useState key: "helpdesk:<name>" cho list, "helpdesk:<name>:loaded" cho cờ đã fetch — giữ đúng convention để tránh đụng key với store khác.
Cuối cùng thêm hàm CRUD tương ứng vào use-helpdesk-api.ts (theo khuôn Teams đã có trong Frontend), rồi wire component/page tiêu thụ store mới.
Đọc tiếp
Phần tiêu đề “Đọc tiếp”- Thêm field vào collection có sẵn — khi chỉ cần thêm cột, không cần bảng mới
- Backend · Endpoints, routes & guards — chi tiết
withAccess/action model - Data model — toàn bộ 19 collection hiện có, đối chiếu FK