Bỏ qua, đến nội dung

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:

  • middleware trên Application không phải array đơn giản - nó là instance Toposort<Middleware>, cho phép đăng ký middleware với tag, 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 appThực chất làGhi chú
app.dbthis.mainDataSource.collectionManager.dbInstance Database (Sequelize wrapper)
app.resourceManagerthis.mainDataSource.resourceManagerInstance ResourceManager
app.aclthis.mainDataSource.aclInstance ACL
app.authManagerthis._authManagerTrực tiếp trên Application

API deprecated

Các property cũ vẫn tồn tại nhưng deprecated:

DeprecatedThay bằng
app.resourcerapp.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:

FeatureMô tảTại sao cần
collectionsFilterChỉ 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 CollectionManagerBiến table thành collection runtime
syncFieldsFromDatabase()Đồng bộ field definition từ schemaDetect 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:

  1. Single source of truth - DB, ResourceManager, ACL của hệ thống chính đều ở một nơi.
  2. Consistency - middleware pipeline cho main data source giống data source ngoài.
  3. 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)

ServicePhụ thuộcVai trò
redisManagerRedis connection stringConnection 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
pubSubManagerredisManagerPub/sub giữa instances
syncMessageManagerpubSubManagerĐồng bộ plugin state cross-instance
eventQueue-Async side-effect queue
lockManagerredisManagerDistributed 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):

MiddlewareToposort positionChức năng
auth-authManager.middleware() - xác thực request
validate-filter-params-Chuẩn hoá filter input
parseVariablesafter aclThay thế biến template trong params
dataTemplateafter 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)

MiddlewareSourceTopo tagPositionMô tả
generateReqIdhelper.tsgenerateReqId-UUID → ctx.reqId + header X-Request-Id
audithelper.tsauditafter generateReqIdGhi event kiểm toán
loggerhelper.tslogger-Log request/response (method, URL, status, duration)
bodyParserhelper.tsbodyParserafter loggerParse JSON / form-data / multipart
(helper)helper.ts--Gắn ctx.getBearerToken() utility
i18nhelper.tsi18nbefore corsDetect locale → ctx.i18n
corshelper.tscorsafter bodyParserCORS headers, preflight
extractClientIphelper.tsextractClientIpbefore corsIP thực từ proxy headers
dataWrappinghelper.tsdataWrappingafter corsWrap response { data, meta }. Tắt: options.dataWrapping = false
dataSourcehelper.tsdataSourceafter dataWrappingCầ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.loaded prevent 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:

  1. Core migration trước - tạo/sửa table hệ thống.
  2. Preset plugin migration - plugin nền tảng (ACL, users, collection-manager).
  3. Other plugin migration - plugin nghiệp vụ phụ thuộc preset.
  4. 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

EventTimingDùng cho
beforeLoadTrước khi load pluginPre-configure global state
afterLoadSau khi load tất cả plugin + syncPost-configure, validate dependencies
beforeStartTrước khi app readyWarm cache, check connections
afterStartApp đã readyGhi log, bật cron job, notify
beforeStopTrước khi shutdownFlush buffer, graceful cleanup
afterStopĐã tắt hoàn toànFinal cleanup, log
beforeInstallTrước khi install pluginsPre-install checks
afterInstallSau install xongSeed data, notify admin
afterUpgradeSau upgrade xongCache warm, notify
beforeReloadTrước reload (dev)Cleanup plugin state
afterReloadSau reload (dev)Re-initialize state
maintainingKhi app vào maintaining modeNotify WebSocket clients
__startedInternal - app startedKhông dùng cho plugin
__stoppedInternal - app stoppedKhông dùng cho plugin

PluginManager events

EventTimingPayload
beforeLoadPluginTrước load từng plugin(plugin, options)
afterLoadPluginSau load từng plugin(plugin, options)
beforeInstallPluginTrước install từng plugin(plugin, options)
afterInstallPluginSau install từng plugin(plugin, options)
beforeEnablePluginTrước enable plugin(pluginName)
afterEnablePluginSau enable plugin(pluginName)
beforeDisablePluginTrước disable plugin(pluginName)
afterDisablePluginSau 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()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ạnLỗi thường gặpHành vi
init()DB connection failThrow → app không start
load() - plugin.loadCollectionsCollection definition saiPlugin đó fail, app có thể tiếp tục
load() - plugin.loadResource/action registration lỗiPlugin đó fail, log error
start() - checkInstallChưa installTự gọi install()
install() - db.syncMigration lỗiThrow → install dừng
install() - plugin.installPlugin install lỗiLog error, có thể continue
upgrade() - migrationMigration fail giữa chừngThrow → 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:

  1. Fix migration code.
  2. Chạy lại app upgrade - skip migration đã hoàn thành.
  3. 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