Bỏ qua, đến nội dung

FAQ - Core

Application & Lifecycle

init()load() khác gì nhau?

init()load()
Khi nàoGọi trong constructorGọ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
PluginChưa loadLoad thực tế (beforeLoad → load → afterLoad)
Gọi lạiKhi 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 sau install/upgrade hoặ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?

EventKhi nàoDùng cho
beforeLoadTrước khi load bất kỳ plugin nàoSetup global state, pre-configure
afterLoadSau khi load tất cả pluginKiểm tra dependency, post-configure
beforeStartTrước khi app listenWarm cache, check connection
afterStartApp đã listenGhi log, bật cron job
beforeStopTrước khi shutdownCleanup, flush buffer
beforeLoadPluginTrước load từng pluginPer-plugin pre-processing
afterLoadPluginSau load từng pluginReact 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 createAppProxy tự 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ẽ:

  1. Nhận ra createdBy là relation → tạo include cho association.
  2. Nhận ra department là relation tiếp → tạo nested include.
  3. Đặt where condition trên departments.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:

FeatureMô tả
collectionsFilterChỉ 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ằngScopeVí dụ
Tầng 1 - Koa appapp.use(fn, topo)Mọi HTTP requestRate limit, custom header
Tầng 2 - Data sourcedataSourceManager.use(fn, topo)Request tới resourceCustom 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?

registerActionHandlerregisterPreActionHandler
Vị trí trong chainCuối cùng - handler chínhTrước handler - preprocessing
Thay thế handler?Có - override handler mặc địnhKhông - chạy trước handler
Ví dụCustom list actionValidate 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?

listquery
Trả vềRecords (rows)Aggregate result (count, sum, avg...)
Paramsfilter, sort, fields, paginationmeasures, dimensions, filter
AssociationHỗ trợKhông hỗ trợ (sourceId phải rỗng)
Dùng choCRUD data displayDashboard, 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:

ParamStrategyLý do
filterand-mergeGiữ mọi filter (bảo mật + nghiệp vụ)
fields/whitelistintersectChỉ trả field nằm trong tất cả danh sách (hạn chế exposure)
appendsunionGộp relation cần eager load
sortoverwriteChỉ 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ự match ui.* mà không cần cập nhật role config.
  • Phủ định: !pm:remove loạ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:

  1. Role đúng chưa? Kiểm tra ctx.state.currentRole - có thể user bị gán role anonymous.
  2. Action có allow? Kiểm tra acl.allowManager - action có đăng ký allow không.
  3. Snippet match? Role có snippet matching action path không.
  4. Strategy? Role có strategy mặc định cho action type này không.
  5. 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 dev

Client Application

Server Application vs Client Application - khác gì?

ServerClient
Kế thừaKoa + AsyncEmitterStandalone class
RuntimeNode.jsBrowser
Plugin loaderFilesystem/DBStatic bundle + remote (requirejs)
Data accessDatabase, RepositoryHTTP API (APIClient)
MiddlewareToposort (Koa + data source)Provider chain (React context)
Event systemAsyncEmitter (async)EventTarget (DOM-standard)
StateIn memoryobservable (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:

  1. Gửi lại auth:token nếu token còn tồn tại.
  2. Server trả trạng thái hiện tại (ready/maintaining).
  3. 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?

. 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?

ServerClient
Package@digiforce-nc/data-source-manager@digiforce-nc/client (internal)
Chức năngKoa pipeline, ResourceManager, ACL, RepositoryMetadata UI, collection info cho form/filter
Truy cập DBCó (Sequelize)Không - chỉ gọi API
Importimport { 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?

DeprecatedThay bằngLý do
app.resourcerapp.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 build cho plugin cần thiết. (Dùng bun run buildbun build là 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() trong loadCollections() 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

  1. 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.
  2. Snippet phủ định: có !snippet đang block action.
  3. Fixed params filter quá chặt: action allow nhưng data bị filter hết.
  4. Data source sai: ACL mỗi data source độc lập - config role trên main khô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ị

  1. Component chưa đăng ký: kiểm tra this.app.addComponents() trong load().
  2. Schema chưa reference: UI Schema node cần x-component khớp tên đăng ký.
  3. Plugin chưa load: kiểm tra event plugin:<name>:loaded trong console.
  4. Lỗi trong component: React error boundary catch - xem console cho stack trace.

Đọc tiếp