Giao diện
FAQ - Core
Application & Lifecycle
init() và load() khác gì nhau?
init() | load() | |
|---|---|---|
| Khi nào | Gọi trong constructor | Gọi khi runtime cần bắt đầu |
| Làm gì | Tạo service instances (DB, managers, middleware) | Init plugins → load plugins → (optional) sync DB |
| Plugin | Chưa load | Load thực tế (beforeLoad → load → afterLoad) |
| Gọi lại | Khi reInit() (reload/upgrade) | Khi reload() hoặc lần đầu start |
Tóm lại: init() dựng "khung" 20 bước (logger → mainDataSource → services → PluginManager → middleware → actions → CLI), load() nạp plugin và chuẩn bị runtime.
Xem thêm: Server - Application § init()
Khi nào dùng restart() vs reload()?
reload(): dispose service cũ →init()lại →load()lại. Không tắt HTTP listener. Dùng cho hot reload khi dev.restart():stop()→start(). Tắt hoàn toàn rồi bật lại. Dùng sauinstall/upgradehoặc khi config thay đổi lớn.
Quy tắc đơn giản
Đang dev → reload(). Production → restart().
Plugin lifecycle event nào hay dùng nhất?
| Event | Khi nào | Dùng cho |
|---|---|---|
beforeLoad | Trước khi load bất kỳ plugin nào | Setup global state, pre-configure |
afterLoad | Sau khi load tất cả plugin | Kiểm tra dependency, post-configure |
beforeStart | Trước khi app listen | Warm cache, check connection |
afterStart | App đã listen | Ghi log, bật cron job |
beforeStop | Trước khi shutdown | Cleanup, flush buffer |
beforeLoadPlugin | Trước load từng plugin | Per-plugin pre-processing |
afterLoadPlugin | Sau load từng plugin | React khi plugin cụ thể load xong |
Danh sách đầy đủ 22 event: Server Application - Event catalog
Tại sao pm.load() có hai vòng (beforeLoad + load)?
Plugin B có thể cần metadata từ plugin A trong load(). Nếu gộp thành một vòng, plugin A chưa setup metadata khi B gọi load().
Vòng 1: A.beforeLoad() → B.beforeLoad() → C.beforeLoad()
Vòng 2: A.load() → B.load() → C.load()Tách ra đảm bảo tất cả plugin đã beforeLoad() trước khi bất kỳ plugin nào load(). Điều này đúng cho cả server và client.
_reinitializable flag trên event listener là gì?
Khi reload() hoặc upgrade(), app gọi reInitEvents() để xóa listener cũ - tránh listener bị đăng ký trùng. Chỉ listener có _reinitializable = true bị xóa.
- Listener tạo qua
createAppProxytự có flag này. - Listener plugin tạo thủ công không có flag → persist qua reload.
Nếu plugin cần listener tự xóa khi reload:
typescript
const handler = async () => { /* ... */ };
handler._reinitializable = true;
this.app.on('afterLoad', handler);db.sync() được gọi hai lần khi install - tại sao?
Lần 1 (Application.install): sync core tables. Lần 2 (PluginManager.install): sync plugin tables - vì load() ở giữa có thể thêm collection mới từ plugin. Không gộp được vì plugin collection chưa define lúc lần 1 chạy.
Database & Collection
db.collection() vs db.extendCollection() - khi nào dùng?
db.collection(options): tạo collection mới. Dùng khi plugin sở hữu collection đó.db.extendCollection(options): thêm field/relation vào collection đã có của plugin khác. Dùng khi mở rộng mà không fork.
typescript
// Plugin A - sở hữu collection
this.db.collection({
name: 'orders',
fields: [{ name: 'status', type: 'string' }],
});
// Plugin B - mở rộng collection của A
this.db.extendCollection({
name: 'orders',
fields: [{ name: 'priority', type: 'integer' }],
});Timing
Nếu plugin B load trước A, extend sẽ được lưu vào delayCollectionExtend và tự apply khi A define collection. Không cần lo thứ tự.
Filter parser hỗ trợ relation path như thế nào?
Filter JSON có thể dùng dot path để query qua relation:
json
{
"createdBy.department.name": { "$eq": "Engineering" }
}FilterParser sẽ:
- Nhận ra
createdBylà relation → tạoincludecho association. - Nhận ra
departmentlà relation tiếp → tạo nestedinclude. - Đặt
wherecondition trêndepartments.name.
Kết quả Sequelize:
javascript
{
include: [
{
association: 'createdBy',
include: [{ association: 'department', where: { name: 'Engineering' } }]
}
]
}Tại sao filter merge dùng and-merge thay vì overwrite?
Bảo mật. ACL middleware inject filter ẩn (ví dụ: { createdById: currentUserId } cho "view own"). Nếu merge bằng overwrite, client có thể gửi filter mới ghi đè filter bảo mật → bypass ACL.
And-merge đảm bảo filter từ mọi nguồn đều được giữ:
ACL filter: { createdById: 1 }
Client filter: { status: 'active' }
Kết quả: { $and: [{ createdById: 1 }, { status: 'active' }] }db.sync() có xóa dữ liệu không?
Không trong trường hợp bình thường. db.sync() chỉ:
- Tạo table mới nếu chưa có.
- Thêm column mới nếu collection có field mới.
- Tạo index mới.
Không xóa column, không drop table. Muốn xóa sạch: dùng db.clean({ drop: true }) (chỉ dùng khi install --force).
MainDataSource khác DataSource thường ở điểm nào?
MainDataSource kế thừa SequelizeDataSource và thêm:
| Feature | Mô tả |
|---|---|
collectionsFilter | Chỉ load collection được quản lý (tránh table ngoài) |
readTables() | Đọc tables từ DB schema thực tế |
loadTables() | Biến table thành collection runtime |
syncFieldsFromDatabase() | Detect thay đổi schema ngoài hệ thống |
app.db, app.resourceManager, app.acl thực chất là shortcut tới mainDataSource. Xem Server Application - Property accessors.
Middleware & Toposort
Toposort là gì? Tại sao không dùng thứ tự code?
Khi nhiều plugin thêm middleware, thứ tự đăng ký phụ thuộc thứ tự load plugin - không kiểm soát được. Toposort cho phép khai báo vị trí tương đối:
typescript
app.use(myMiddleware, {
tag: 'myPlugin',
after: 'auth', // chạy sau auth
before: 'acl', // chạy trước ACL
});Hệ thống tự tính thứ tự đúng bất kể plugin load trước hay sau.
Hai tầng middleware - plugin thêm ở tầng nào?
| Tầng | Đăng ký bằng | Scope | Ví dụ |
|---|---|---|---|
| Tầng 1 - Koa app | app.use(fn, topo) | Mọi HTTP request | Rate limit, custom header |
| Tầng 2 - Data source | dataSourceManager.use(fn, topo) | Request tới resource | Custom auth, data filter |
Phần lớn plugin dùng tầng 2 vì logic nghiệp vụ (auth, ACL, data) nằm ở đó.
Auth/ACL nằm ở tầng nào? Tại sao?
Nằm trong tầng 2 (data source pipeline), không phải tầng 1 (Koa app). Mỗi data source có thể có ACL riêng - nếu ACL ở tầng Koa, chỉ có một bộ quyền chung cho tất cả data source.
Resourcer & Actions
registerActionHandler vs registerPreActionHandler - khi nào dùng?
registerActionHandler | registerPreActionHandler | |
|---|---|---|
| Vị trí trong chain | Cuối cùng - handler chính | Trước handler - preprocessing |
| Thay thế handler? | Có - override handler mặc định | Không - chạy trước handler |
| Ví dụ | Custom list action | Validate input, inject filter |
typescript
// Override handler cho action "export" trên resource "orders"
resourceManager.registerActionHandler('orders:export', async (ctx) => {
// custom export logic
});
// Pre-handler cho tất cả "create" actions
resourceManager.registerPreActionHandler('create', async (ctx, next) => {
// validate input
await next();
}, { after: 'acl' });Action query khác list như thế nào?
list | query | |
|---|---|---|
| Trả về | Records (rows) | Aggregate result (count, sum, avg...) |
| Params | filter, sort, fields, pagination | measures, dimensions, filter |
| Association | Hỗ trợ | Không hỗ trợ (sourceId phải rỗng) |
| Dùng cho | CRUD data display | Dashboard, chart, report |
Tại sao mergeParams dùng nhiều strategy khác nhau?
Mỗi loại param có ngữ nghĩa riêng:
| Param | Strategy | Lý do |
|---|---|---|
filter | and-merge | Giữ mọi filter (bảo mật + nghiệp vụ) |
fields/whitelist | intersect | Chỉ trả field nằm trong tất cả danh sách (hạn chế exposure) |
appends | union | Gộp relation cần eager load |
sort | overwrite | Chỉ có một thứ tự sort cuối cùng |
ACL & Auth
Snippet là gì? Tại sao không dùng role → action trực tiếp?
Snippet nhóm nhiều action path theo glob pattern. Thay vì gán 50 action riêng lẻ cho role, gán 1 snippet:
Snippet "ui.*" → match: ui:getSchema, ui:updateSchema, ui:deleteSchema, ...
Snippet "pm.*" → match: pm:list, pm:enable, pm:disable, ...Lợi ích:
- Gọn: 1 snippet thay 50 permission entries.
- Dynamic: plugin mới thêm action
ui:newFeature→ tự matchui.*mà không cần cập nhật role config. - Phủ định:
!pm:removeloại trừ action cụ thể khỏi snippet.
acl.allow() vs acl.define() - khác gì?
acl.allow(resource, actions, condition): đăng ký ngoại lệ - resource/action này được phép mà không cần role (public, loggedIn...).acl.define({ role, strategy, actions, snippets }): định nghĩa role với bộ quyền cụ thể.
typescript
// Cho phép ai cũng truy cập (không cần đăng nhập)
acl.allow('auth', ['signIn', 'signUp'], 'public');
// Định nghĩa role "editor" với quyền cụ thể
acl.define({
role: 'editor',
actions: { 'posts:create': {}, 'posts:update': { own: true } },
snippets: ['ui.*'],
});Tại sao auth middleware nằm trước ACL?
Auth xác định ai đang gọi (currentUser, currentRole). ACL cần thông tin này để kiểm tra quyền. Nếu ACL chạy trước auth, không biết role của user → không thể phân quyền.
auth → set ctx.state.currentUser + currentRole
↓
ACL → dùng currentRole để can(role, resource, action)Làm sao debug lỗi 403 Forbidden?
Checklist:
- Role đúng chưa? Kiểm tra
ctx.state.currentRole- có thể user bị gán roleanonymous. - Action có allow? Kiểm tra
acl.allowManager- action có đăng kýallowkhông. - Snippet match? Role có snippet matching action path không.
- Strategy? Role có strategy mặc định cho action type này không.
- Fixed params? Nếu allow nhưng fixed params filter quá chặt → trả 0 record (không phải 403 nhưng "mất" data).
Bật log:
bash
DEBUG=acl* bun devClient Application
Server Application vs Client Application - khác gì?
| Server | Client | |
|---|---|---|
| Kế thừa | Koa + AsyncEmitter | Standalone class |
| Runtime | Node.js | Browser |
| Plugin loader | Filesystem/DB | Static bundle + remote (requirejs) |
| Data access | Database, Repository | HTTP API (APIClient) |
| Middleware | Toposort (Koa + data source) | Provider chain (React context) |
| Event system | AsyncEmitter (async) | EventTarget (DOM-standard) |
| State | In memory | observable (reactive) |
Xem thêm: Client - Application
Remote plugin load bằng gì? Có cache không?
Remote plugin build thành UMD bundle. Client dùng requirejs để tải qua HTTP. Browser cache theo HTTP cache header (Cache-Control).
Flow: API pm:listEnabled → lấy URL bundle → requirejs tải → resolve plugin class → PluginManager.add().
Nếu cần force reload (sau upgrade plugin): đổi version trong URL hoặc clear browser cache.
Provider chain ảnh hưởng performance không?
Không đáng kể. Mỗi provider là React context wrapper:
- Context chỉ re-render subscriber khi value thay đổi.
- Provider chain build một lần khi mount, không rebuild mỗi render.
- Performance bottleneck thường là data fetch hoặc schema rendering phức tạp, không phải provider count.
WebSocket mất kết nối thì sao?
WebSocketClient tự reconnect với exponential backoff. Khi reconnect:
- Gửi lại
auth:tokennếu token còn tồn tại. - Server trả trạng thái hiện tại (ready/maintaining).
- Client cập nhật UI tương ứng.
Nếu server đang maintaining khi reconnect → client hiển thị UI maintaining cho đến khi nhận signal maintaining = false.
Plugin client lỗi có crash toàn app không?
Có thể. Khác server (plugin error có thể isolate), client plugin error trong load() sẽ block toàn bộ app - vào error state, hiển thị error page.
Plugin client cần handle error cẩn thận:
typescript
class MyPlugin extends Plugin {
async load() {
try {
// risky initialization
} catch (e) {
console.error('MyPlugin load failed:', e);
// graceful fallback thay vì throw
}
}
}Làm sao plugin đăng ký component, route, settings?
Trong method load() của plugin:
typescript
class MyPlugin extends Plugin {
async load() {
// Component cho Schema Renderer
this.app.addComponents({ MyBlock: MyBlockComponent });
// Route SPA
this.app.router.add('admin.myPlugin', {
path: '/admin/my-plugin',
Component: MyPluginPage,
});
// Schema settings (menu cấu hình block)
this.app.schemaSettingsManager.add('myBlockSettings', {
items: [/* ... */],
});
// Schema initializer (nút "thêm block")
this.app.schemaInitializerManager.add('myBlockInit', {
items: [/* ... */],
});
// Settings page trong admin
this.app.pluginSettingsManager.add('my-plugin', {
title: 'My Plugin',
icon: 'SettingOutlined',
Component: MySettingsPage,
});
// Provider tùy chỉnh
this.app.addProviders([MyProvider]);
}
}DataSourceManager
Data source ngoài có ACL riêng không?
Có. Mỗi DataSource tạo instance ACL riêng trong init(). Nghĩa là quyền trên data source main và data source external-1 hoàn toàn độc lập.
Tuy nhiên, plugin-acl mặc định chỉ quản lý ACL cho main. Data source ngoài cần plugin riêng hoặc custom code để sync cấu hình quyền.
collectionToResourceMiddleware là gì?
Middleware này tự động tạo resource cho collection khi request đến. Khi collection orders tồn tại nhưng chưa có resource tương ứng, middleware sẽ resourceManager.define({ name: 'orders' }) on-the-fly.
Nhờ đó:
- Tạo collection mới qua UI → API endpoint tự có ngay.
- Không cần plugin define resource thủ công cho mỗi collection.
Server DataSourceManager vs Client DataSourceManager?
| Server | Client | |
|---|---|---|
| Package | @digiforce-nc/data-source-manager | @digiforce-nc/client (internal) |
| Chức năng | Koa pipeline, ResourceManager, ACL, Repository | Metadata UI, collection info cho form/filter |
| Truy cập DB | Có (Sequelize) | Không - chỉ gọi API |
| Import | import { DataSourceManager } from '@digiforce-nc/data-source-manager' | import { DataSourceManager } from '@digiforce-nc/client' |
Import nhầm
Hai class cùng tên nhưng khác hoàn toàn. Nếu import nhầm, TypeScript sẽ báo lỗi method không tồn tại.
Deprecated API
Những API nào đã deprecated?
| Deprecated | Thay bằng | Lý do |
|---|---|---|
app.resourcer | app.resourceManager | Đổi tên cho nhất quán |
app.collection(...) | app.db.collection(...) | Tách rõ trách nhiệm |
app.resource(...) | app.resourceManager.define(...) | Tách rõ trách nhiệm |
app.actions(...) | app.resourceManager.registerActionHandler(...) | API rõ ràng hơn |
acl.skip(...) | acl.allow(...) | Đổi tên cho rõ nghĩa |
API cũ vẫn hoạt động nhưng sẽ bị xóa trong phiên bản tương lai. Migrate sớm.
Lỗi thường gặp
Cannot find module '@digiforce-nc/...'
- Chưa install dependencies: chạy
bun installở root. - Plugin chưa build: chạy
bun run buildcho plugin cần thiết. (Dùngbun run buildvìbun buildlà lệnh bundler riêng của Bun.) - Monorepo symlink hỏng: chạy
bun install --force.
collection 'xxx' not found
- Collection chưa được define: kiểm tra plugin có gọi
db.collection()trongloadCollections()không. - Plugin chưa enable: kiểm tra trạng thái plugin trong admin.
- Data source sai: request đang gửi tới data source không chứa collection này (kiểm tra header
x-data-source).
No permission nhưng đã config role
- Role config chưa sync: plugin-acl đọc DB → sync vào engine. Nếu config mới nhưng chưa sync → engine chưa biết.
- Snippet phủ định: có
!snippetđang block action. - Fixed params filter quá chặt: action allow nhưng data bị filter hết.
- Data source sai: ACL mỗi data source độc lập - config role trên
mainkhông áp dụng cho data source khác.
Migration fail khi upgrade
- Kiểm tra thứ tự: core migration chạy trước plugin migration. Nếu plugin depend collection chưa có → fail.
- Kiểm tra version: Umzug track migration đã chạy. Migration cùng tên sẽ skip.
- Rollback: hệ thống không auto rollback. Cần restore DB từ backup rồi fix migration code.
Xem thêm: Server - Application § upgrade()
Plugin client load xong nhưng component không hiển thị
- Component chưa đăng ký: kiểm tra
this.app.addComponents()trongload(). - Schema chưa reference: UI Schema node cần
x-componentkhớp tên đăng ký. - Plugin chưa load: kiểm tra event
plugin:<name>:loadedtrong console. - Lỗi trong component: React error boundary catch - xem console cho stack trace.