Giao diện
Server - Application
1) Application - class trung tâm
Kế thừa và mixin
Application vừa là HTTP server (Koa) vừa là event bus (AsyncEmitter). Điểm đặc biệt:
middlewaretrên Application không phải array đơn giản - nó là instanceToposort<Middleware>, cho phép đăng ký middleware vớitag,before,after.- Koa gốc dùng array → thứ tự theo
app.use(). Digiforce override để dùng topological sort → plugin thêm middleware an toàn.
Property accessors - shortcut tới MainDataSource
Property trên app | Thực chất là | Ghi chú |
|---|---|---|
app.db | this.mainDataSource.collectionManager.db | Instance Database (Sequelize wrapper) |
app.resourceManager | this.mainDataSource.resourceManager | Instance ResourceManager |
app.acl | this.mainDataSource.acl | Instance ACL |
app.authManager | this._authManager | Trực tiếp trên Application |
API deprecated
Các property cũ vẫn tồn tại nhưng deprecated:
| Deprecated | Thay bằng |
|---|---|
app.resourcer | app.resourceManager |
app.collection(...) | app.db.collection(...) |
app.resource(...) | app.resourceManager.define(...) |
app.actions(...) | app.resourceManager.registerActionHandler(...) |
Dùng API mới để tương thích phiên bản tương lai.
2) createMainDataSource() - khởi tạo data layer
Method này tạo toàn bộ data layer cốt lõi. Được gọi bên trong init().
MainDataSource khác DataSource thông thường
MainDataSource (trong main-data-source.ts) kế thừa SequelizeDataSource và thêm:
| Feature | Mô tả | Tại sao cần |
|---|---|---|
collectionsFilter | Chỉ load collection được quản lý | Tránh load table ngoài (legacy, system table) |
readTables() | Đọc tables từ DB schema thực tế | Cho UI "Connect to existing table" |
loadTables() | Load table vào CollectionManager | Biến table thành collection runtime |
syncFieldsFromDatabase() | Đồng bộ field definition từ schema | Detect thay đổi schema ngoài hệ thống |
| ACL middleware group | { group: 'acl', after: 'auth' } | ACL luôn chạy sau auth trong pipeline |
Tại sao tất cả đi qua MainDataSource?
Thiết kế này đảm bảo:
- Single source of truth - DB, ResourceManager, ACL của hệ thống chính đều ở một nơi.
- Consistency - middleware pipeline cho main data source giống data source ngoài.
- Replaceable - nếu cần swap DB engine, chỉ thay MainDataSource implementation.
3) init() - trình tự khởi tạo chi tiết
Giải thích từng giai đoạn
Phase 1: Foundation (bước 1-2)
initLogger() - Dựng logger cho toàn bộ app. Mọi bước sau đều cần log.
reInitEvents() - Reset middleware Toposort (this.middleware = new Toposort<Middleware>()). Xóa tất cả event listener có flag _reinitializable = true.
Tại sao reinitializable?
Khi reload() hoặc upgrade(), app cần init() lại. Nếu không xóa listener cũ → listener bị đăng ký trùng → logic chạy nhiều lần. Flag _reinitializable đánh dấu listener nào cần xóa khi reinit.
Phase 2: Data layer (bước 3)
createMainDataSource(options) - Tạo MainDataSource → DataSourceManager → set main. Chi tiết xem mục 2.
Phase 3: Infrastructure services (bước 4-12)
| Service | Phụ thuộc | Vai trò |
|---|---|---|
redisManager | Redis connection string | Connection pool cho cache/pubsub/lock |
workerIdAllocator | - | Cấp phát worker ID cho snowflake field (distributed unique) |
cronManager | - | Lịch chạy tác vụ định kỳ |
env | - | Environment variables wrapper |
i18n | - | Internationalization |
pubSubManager | redisManager | Pub/sub giữa instances |
syncMessageManager | pubSubManager | Đồng bộ plugin state cross-instance |
eventQueue | - | Async side-effect queue |
lockManager | redisManager | Distributed lock |
Thứ tự quan trọng
pubSubManager cần redisManager → redis phải init trước. syncMessageManager cần pubSubManager → pubsub phải init trước sync. Đảo thứ tự sẽ crash.
Phase 4: Plugin Manager (bước 13)
new PluginManager({ app, plugins }) - Tạo PM, truyền app instance và danh sách plugin từ options. PM chưa load plugin ở bước này - chỉ khởi tạo.
Phase 5: Security (bước 14)
initAuthManager() - Tạo AuthManager, gắn JwtService. Define resource auth (cho endpoint /auth:signIn, /auth:signUp...).
initAuditManager() - Tạo AuditManager cho event audit logging.
Phase 6: Data source hooks (bước 15-16)
Hai hook này chạy mỗi khi bất kỳ data source nào được add - kể cả main và data source ngoài.
Phase 7: Data source middleware (bước 17)
Middleware đăng ký vào data source pipeline (không phải Koa app):
| Middleware | Toposort position | Chức năng |
|---|---|---|
auth | - | authManager.middleware() - xác thực request |
validate-filter-params | - | Chuẩn hoá filter input |
parseVariables | after acl | Thay thế biến template trong params |
dataTemplate | after acl | Áp dụng data template |
Phase 8: Koa middleware (bước 18)
registerMiddlewares(this, options) gắn middleware Koa app-level. Chi tiết xem Architecture: Hai tầng middleware.
Phase 9: Default actions (bước 19)
registerActions(this) nạp handler chuẩn từ @digiforce-nc/actions: list, get, create, update, destroy, set, add, remove, query.
Phase 10: CLI (bước 20)
registerCli(this) đăng ký CLI commands: start, install, upgrade, pm, migrator, destroy.
4) Middleware stack chi tiết
Koa app-level (registerMiddlewares trong helper.ts)
| Middleware | Source | Topo tag | Position | Mô tả |
|---|---|---|---|---|
generateReqId | helper.ts | generateReqId | - | UUID → ctx.reqId + header X-Request-Id |
audit | helper.ts | audit | after generateReqId | Ghi event kiểm toán |
logger | helper.ts | logger | - | Log request/response (method, URL, status, duration) |
bodyParser | helper.ts | bodyParser | after logger | Parse JSON / form-data / multipart |
| (helper) | helper.ts | - | - | Gắn ctx.getBearerToken() utility |
i18n | helper.ts | i18n | before cors | Detect locale → ctx.i18n |
cors | helper.ts | cors | after bodyParser | CORS headers, preflight |
extractClientIp | helper.ts | extractClientIp | before cors | IP thực từ proxy headers |
dataWrapping | helper.ts | dataWrapping | after cors | Wrap response { data, meta }. Tắt: options.dataWrapping = false |
dataSource | helper.ts | dataSource | after dataWrapping | Cầu nối sang tầng 2 - chọn data source, delegate pipeline |
Data source pipeline middleware
Gắn trong init() bước 17. Chạy bên trong data source, sau khi dataSource middleware (tầng 1) chọn source.
Plugin thêm middleware
Plugin thêm middleware vào data source pipeline:
typescript
// Trong plugin.load()
this.app.dataSourceManager.use(async (ctx, next) => {
// logic trước handler
await next();
// logic sau handler
}, { tag: 'myPlugin', after: 'auth', before: 'acl' });5) Lifecycle methods
State machine tổng thể
load(options) - nạp plugin và chuẩn bị runtime
Hai vòng trong pm.load():
- Vòng 1 (
beforeLoad): Mỗi plugin chuẩn bị context/config. Chưa đăng ký collection hay action. - Vòng 2 (
load): Plugin đăng ký collection, resource, action, middleware.state.loadedprevent double-load.
Tại sao tách hai vòng?
Plugin B có thể cần metadata từ plugin A trong load(). Nếu gộp beforeLoad + load thành một vòng, plugin A chưa setup metadata khi B gọi load(). Tách ra đảm bảo tất cả plugin đã beforeLoad trước khi bất kỳ plugin nào load.
start(options) - bắt đầu nhận request
stop(options) - tắt server
install(options) - cài đặt mới
Tại sao db.sync() hai lần?
Lần 1 (Application): sync core tables. Lần 2 (PluginManager): sync plugin tables - vì load() ở giữa có thể thêm collection mới từ plugin.
upgrade(options) - nâng cấp nhiều pha
Tại sao nhiều pha?
Upgrade phải tôn trọng dependency giữa migration:
- Core migration trước - tạo/sửa table hệ thống.
- Preset plugin migration - plugin nền tảng (ACL, users, collection-manager).
- Other plugin migration - plugin nghiệp vụ phụ thuộc preset.
- afterLoad migration - migration cần toàn bộ collection đã load.
Nếu gộp thành một pha, plugin B migration đọc table của plugin A - nhưng A chưa migrate.
reload() - hot reload (dev)
Không tắt HTTP listener - server vẫn nhận request. Dùng cho development khi thay đổi plugin code.
restart() - khởi động lại hoàn toàn
Tắt hoàn toàn rồi bật lại. Dùng sau install, upgrade, hoặc khi cần reset state.
6) Event catalog
Bảng đầy đủ tất cả event mà Application emit trong lifecycle:
Application events
| Event | Timing | Dùng cho |
|---|---|---|
beforeLoad | Trước khi load plugin | Pre-configure global state |
afterLoad | Sau khi load tất cả plugin + sync | Post-configure, validate dependencies |
beforeStart | Trước khi app ready | Warm cache, check connections |
afterStart | App đã ready | Ghi log, bật cron job, notify |
beforeStop | Trước khi shutdown | Flush buffer, graceful cleanup |
afterStop | Đã tắt hoàn toàn | Final cleanup, log |
beforeInstall | Trước khi install plugins | Pre-install checks |
afterInstall | Sau install xong | Seed data, notify admin |
afterUpgrade | Sau upgrade xong | Cache warm, notify |
beforeReload | Trước reload (dev) | Cleanup plugin state |
afterReload | Sau reload (dev) | Re-initialize state |
maintaining | Khi app vào maintaining mode | Notify WebSocket clients |
__started | Internal - app started | Không dùng cho plugin |
__stopped | Internal - app stopped | Không dùng cho plugin |
PluginManager events
| Event | Timing | Payload |
|---|---|---|
beforeLoadPlugin | Trước load từng plugin | (plugin, options) |
afterLoadPlugin | Sau load từng plugin | (plugin, options) |
beforeInstallPlugin | Trước install từng plugin | (plugin, options) |
afterInstallPlugin | Sau install từng plugin | (plugin, options) |
beforeEnablePlugin | Trước enable plugin | (pluginName) |
afterEnablePlugin | Sau enable plugin | (pluginName) |
beforeDisablePlugin | Trước disable plugin | (pluginName) |
afterDisablePlugin | Sau disable plugin | (pluginName) |
Sử dụng event trong plugin
typescript
class MyPlugin extends Plugin {
async load() {
// Chạy sau khi TẤT CẢ plugin đã load
this.app.on('afterLoad', async () => {
// Kiểm tra dependency, post-configure
});
// Chạy khi một plugin cụ thể load xong
this.app.on('afterLoadPlugin', async (plugin) => {
if (plugin.name === 'plugin-acl') {
// Setup ACL integration
}
});
}
}7) PluginManager - integration chi tiết
Collection và resource nội bộ
PluginManager tự quản lý:
- Collection
applicationPlugins- lưu trạng thái plugin trong DB. - Resource
pm- REST API quản lý plugin. - Public actions (qua ACL):
pm:listEnabled,pm:listEnabledV2.
pm.load(options) - hai vòng
pm.install(options) - install plugin
pm.enable(pluginNames) - bật plugin
8) CLI command lifecycle
createCLI() và AppCommand
AppCommand extends Commander và gắn hook:
runAsCLI(argv) - entry point
Logic reset option state rất quan trọng khi chạy test - nhiều lần runAsCLI trong cùng process.
9) Error handling trong lifecycle
| Giai đoạn | Lỗi thường gặp | Hành vi |
|---|---|---|
init() | DB connection fail | Throw → app không start |
load() - plugin.loadCollections | Collection definition sai | Plugin đó fail, app có thể tiếp tục |
load() - plugin.load | Resource/action registration lỗi | Plugin đó fail, log error |
start() - checkInstall | Chưa install | Tự gọi install() |
install() - db.sync | Migration lỗi | Throw → install dừng |
install() - plugin.install | Plugin install lỗi | Log error, có thể continue |
upgrade() - migration | Migration fail giữa chừng | Throw → cần fix + chạy lại |
Không auto-rollback
Migration fail giữa chừng không tự rollback. Umzug track migration đã chạy thành công. Cần:
- Fix migration code.
- Chạy lại
app upgrade- skip migration đã hoàn thành. - Hoặc restore DB từ backup.
10) reInitEvents() - reinitializable listener
Listener tạo qua createAppProxy tự gắn _reinitializable = true. Khi app reload, những listener này được xóa → tránh duplicate handler.
Plugin tạo listener thủ công không có flag này → persist qua reload. Nếu plugin cần listener tự xóa khi reload, phải gắn flag:
typescript
const handler = async () => { /* ... */ };
handler._reinitializable = true;
this.app.on('afterLoad', handler);Đọc tiếp
- Client - Application - SPA lifecycle tương ứng
- Resourcer & Actions - action pipeline chi tiết
- DataSourceManager - multi data source
- Engine ACL - phân quyền runtime
- Auth & AuthManager - xác thực
- Architecture: Server - cái nhìn kiến trúc tổng thể
- FAQ - câu hỏi thường gặp