Bỏ qua, đến nội dung

Kiến trúc Server

Server Digiforce là ứng dụng Node.js dựa trên Koa, mở rộng bằng AsyncEmitter. Trang này mô tả cấu trúc bên trong server - từ service graph, hai tầng middleware, resource/action pipeline, đến data source routing và lifecycle management.

Điều kiện đọc

Đọc trước Tổng quan hệ thống để nắm boundary và system context. Sau trang này, đọc Security modelRequest lifecycle để hoàn thiện bức tranh server.


1) Application - trung tâm server

Application kế thừa Koa (HTTP framework) và mixin AsyncEmitter (event bus hỗ trợ async listener). Mọi service, manager, middleware đều gắn vào instance này.

Service graph

Service relationships

LayerServicePhụ thuộcGhi chú
DataDataSourceManager-Quản lý tất cả data source (main + external)
MainDataSourceDatabase, ACL, ResourceManagerData source mặc định, tạo trong init()
DatabaseSequelizeORM layer, quản lý collection/model/repository
APIResourceManager-Parse URL → Resource → Action, middleware pipeline
Action handlersResourceManagerlist, get, create, update, destroy, query
SecurityAuthManagerJwtServiceXác thực request, quản lý auth types
ACL-Engine phân quyền runtime (in-memory)
PluginPluginManagerApplicationLoad, install, enable/disable plugin
CLIPluginManagerCLI commands cho vận hành
ApplicationVersionDatabaseTrack version app + plugins
InfraCacheManagerRedis (optional)Cache API, wrap pattern
PubSubManagerRedisCross-instance messaging
LockManagerRedisDistributed lock

MainDataSource là trung tâm

db, resourceManager, acl mà bạn thấy trên app thực chất là shortcut tới mainDataSource.collectionManager.db, mainDataSource.resourceManager, mainDataSource.acl. Không tạo riêng - mọi thứ đi qua MainDataSource.

Trình tự init() - tại sao thứ tự quan trọng

Thứ tự không ngẫu nhiên - mỗi bước phụ thuộc kết quả bước trước:

BướcTại sao phải ở vị trí này
1. LoggerTất cả bước sau cần log. Phải dựng đầu tiên.
2. MainDataSourceTạo DB, ResourceManager, ACL. Mọi service sau đều cần ít nhất một trong ba.
3. Service nềnRedis/pubsub cần cho cache manager và lock. Worker ID cần cho snowflake field.
4. PluginManagerCần DB (từ bước 2) để đọc danh sách plugin đã cài.
5. AuthManagerCần ResourceManager (từ bước 2) để define resource auth.
6. HooksCần PluginManager + ACL đã có. Hook sync availableActions vào ACL khi data source mới được add.
7. DS middlewareCần AuthManager (bước 5) để gắn auth middleware vào pipeline.
8. App middlewareGắn sau DS middleware vì middleware dataSource (tầng 1) phải biết DataSourceManager đã setup.
9. ActionsRegister sau middleware vì handler cần middleware chain đã đầy đủ.
10. CLICuối cùng vì CLI command có thể trigger bất kỳ flow nào ở trên.

2) Hai tầng middleware - thiết kế cốt lõi

Request API đi qua hai tầng middleware tách biệt. Đây là quyết định kiến trúc quan trọng nhất của server.

Tại sao hai tầng?

Tầng 1 - Koa appTầng 2 - Data source pipeline
ScopeMọi HTTP requestRequest tới resource/action
Quan tâmParse body, CORS, logging, format responseAuth, ACL, data operation
Đăng ký bằngapp.use(fn, topoTags)dataSourceManager.use(fn) hoặc resourceManager.use(fn)
Có per-source?Không - chung cho mọi request - mỗi DataSource có pipeline riêng
Auth/ACLKhôngCó - nằm ở đây

Tại sao Auth/ACL nằm trong data source?

Vì mỗi data source có thể có ACL riêng (ví dụ: data source ngoài có quy tắc phân quyền khác). Nếu ACL nằm ở tầng Koa, chỉ có một bộ quyền chung cho tất cả - không linh hoạt.

Tầng 1 - Koa app middleware (registerMiddlewares)

Chi tiết từng middleware:

TagVị trí ToposortChức năngGhi chú
generateReqIdĐầu tiênGắn UUID vào ctx.reqId và header X-Request-IdTất cả log sau đều kèm ID này
auditafter generateReqIdGhi sự kiện kiểm toán (ai làm gì)Tùy cấu hình audit plugin
loggerafter auditLog request: method, URL, status, durationTích hợp @digiforce-nc/logger
bodyParserafter loggerParse application/jsonmultipart/form-dataCó thể tắt cho route cụ thể
(helper)after bodyParserGắn ctx.getBearerToken() utilityKhông phải middleware riêng, gắn kèm
i18nbefore corsDetect locale từ header/query, gắn ctx.i18nẢnh hưởng error message ngôn ngữ
corsafter bodyParserCORS headers, preflight handlingConfig từ env hoặc app options
extractClientIpbefore corsLấy IP thực từ X-Forwarded-For / X-Real-IPCần khi app sau proxy/LB
dataWrappingafter corsWrap response thành { data, meta } formatCó thể tắt (options.dataWrapping = false)
dataSourceafter dataWrappingChọn data source → delegate sang tầng 2Middleware quan trọng nhất - cầu nối hai tầng

Toposort, không phải thứ tự code

Thứ tự thực thi không phụ thuộc vào thứ tự gọi app.use() trong code. Hệ thống dùng Toposort với tag, after, before để tính thứ tự. Plugin thêm middleware cần khai báo dependency:

typescript
app.use(myMiddleware, {
  tag: 'myPlugin',
  after: 'auth',
  before: 'acl',
});

Tầng 2 - Data source pipeline

Sau khi middleware dataSource chọn source (header x-data-source, mặc định main), request đi vào pipeline nội bộ của data source đó.

MiddlewareTag ToposortChức năng chi tiết
validate-filter-paramsĐầu pipelineKiểm tra và chuẩn hoá filter input - chặn filter injection sớm
authafter validateAuthManager.middleware() - verify JWT, resolve user, gắn ctx.state.currentUser + ctx.state.currentRole
ResourceManagerafter authParse URL path → resolve Resource + Action. Gắn ctx.action với actionName, resourceName, params
aclafter ResourceManagerACL.middleware() - kiểm tra quyền, merge fixed params (filter bổ sung theo role)
parseVariablesafter aclThay thế biến template (như {{ $user.id }}) trong params bằng giá trị thực
dataTemplateafter aclÁp dụng data template cho action
Pre-action handlersafter dataTemplatePlugin đăng ký qua registerPreActionHandler - validate input, inject logic
Action handlerCuốiHandler chính - gọi Repository (find, create, update, destroy...)

Auth và ACL bên trong data source

Khác với nhiều framework đặt auth ở app-level, Digiforce đặt auth/ACL bên trong data source pipeline. Hệ quả:

  • Request không tới resource (ví dụ: static file, health check) không đi qua auth/ACL.
  • Mỗi data source có thể có ACL policy hoàn toàn khác nhau.
  • Plugin chỉ cần gắn middleware vào data source pipeline, không cần lo tầng Koa.

Koa onion model áp dụng cả hai tầng

Cả Koa middleware (tầng 1) và data source middleware (tầng 2) đều dùng onion model - middleware được gọi hai lần (vào và ra):

Khi handler trả kết quả:

  1. Response đi ngược qua ACL (có thể filter response fields).
  2. Đi ngược qua dataWrapping (wrap response format).
  3. Đi ngược qua logger (ghi response status + duration).

Middleware có thể can thiệp cả chiều vào lẫn chiều ra - đây là sức mạnh của onion model.


3) ResourceManager - parse request và chạy action

ResourceManager (@digiforce-nc/resourcer) là engine chuyển HTTP request thành action execution.

URL parsing - hai mode

Mode 1: Explicit route

POST /resourcer/users:list
POST /resourcer/orders:export

Resource và action được chỉ định trực tiếp. Dùng cho action tùy chỉnh không map sang REST method.

Mode 2: REST-like routes

URL patternHTTP MethodActionVí dụ
/:resourceGETlistGET /api/users
/:resource/:idGETgetGET /api/users/1
/:resourcePOSTcreatePOST /api/users
/:resource/:idPUT / PATCHupdatePUT /api/users/1
/:resource/:idDELETEdestroyDELETE /api/users/1
/:assoc/:assocId/:resourceGETlist (association)GET /api/users/1/orders
/:assoc/:assocId/:resource/:idGETget (association)GET /api/users/1/orders/5
/:assoc/:assocId/:resourcePOSTcreate / set / addPOST /api/users/1/roles
/:assoc/:assocId/:resource/:idDELETEdestroy / removeDELETE /api/users/1/roles/3

Action cho association resource phụ thuộc relation type (hasMany, belongsToMany...).

Handler chain - thứ tự thực thi

Khi request được parse thành Resource + Action, Action.getHandlers() build handler chain:

Vị tríĐăng ký bằngScopeVí dụ
Global middlewareresourceManager.use(fn, topo)Mọi resourceLogging, rate limit
Resource middlewareresource.use(fn)Resource cụ thểCustom header check
Action middlewareaction.use(fn)Action cụ thểFile upload handler
Pre-actionregisterPreActionHandler(name, fn, topo)Theo action nameValidate, inject filter
HandlerregisterActionHandler(name, fn)Theo action nameCRUD handler chính

mergeParams - chiến lược merge thông minh

Action.mergeParams() gom params từ nhiều nguồn (querystring, path, body, middleware) bằng chiến lược đặc thù:

ParamStrategyLý do
filterand-mergeGiữ mọi filter (bảo mật + nghiệp vụ), không cho ghi đè
fields / whitelist / blacklistintersectChỉ giữ field nằm trong tất cả danh sách
appends / exceptunionGộp relation cần eager load
sortoverwriteChỉ có một thứ tự sort cuối cùng

Tại sao filter dùng and-merge?

ACL middleware inject filter ẩn (ví dụ { createdById: currentUserId } cho quyền "view own"). Nếu merge bằng overwrite, client có thể gửi filter mới bypass filter bảo mật. And-merge đảm bảo mọi filter đều được giữ.


4) DataSourceManager - đa data source

Kiến trúc

Mỗi DataSourceđơn vị độc lập với bộ ba: CollectionManager + ResourceManager + ACL. Nghĩa là:

  • Collection users trên mainusers trên external-pg hoàn toàn khác nhau (khác schema, khác quyền).
  • ACL policy trên main không áp dụng cho external-pg.

Request routing qua data source

DataSource.init() - setup mỗi data source

Auto resource từ collection

Middleware collectionToResourceMiddleware() tự động tạo resource khi:

  1. URL parse được ra resource hợp lệ.
  2. Collection tồn tại trong CollectionManager.
  3. Resource chưa được define.

Nhờ cơ chế này, tạo collection mới qua UI → API endpoint tự có ngay mà không cần code.

MainDataSource - đặc biệt ở chỗ nào?

MainDataSource (trong packages/core/server/src/main-data-source.ts) có thêm:

FeatureMô tả
collectionsFilterChỉ load collection được quản lý (không load table ngoài)
readTables()Đọc tables từ DB schema
loadTables()Load tables vào CollectionManager
syncFieldsFromDatabase()Đồng bộ field definition từ DB schema thực tế

Application luôn tạo sẵn MainDataSource tên main trong createMainDataSource(). Đây là data source duy nhất bắt buộc.


5) Infrastructure services

Sơ đồ tương tác (multi-instance)

Chi tiết từng service

CacheManager (@digiforce-nc/cache)

FeatureMô tả
BackendIn-memory (single instance) hoặc Redis (multi-instance)
APIget, set, del, wrap (cache-aside pattern)
NamespaceHỗ trợ namespace cho từng plugin
TTLConfigurable per key

PubSubManager

FeatureMô tả
BackendRedis pub/sub
Mục đíchBroadcast event giữa instances: plugin state change, cache invalidation
APIpublish(channel, message), subscribe(channel, handler)

Bắt buộc Redis khi multi-instance

Nếu chạy nhiều instance mà không có Redis, mỗi instance sẽ có cache riêng, plugin state riêng → data inconsistency. Redis là dependency bắt buộc cho production multi-instance.

SyncMessageManager

FeatureMô tả
Xây trênPubSubManager
Mục đíchĐồng bộ trạng thái plugin, cache invalidation có ordering
Khác PubSubĐảm bảo message được xử lý theo thứ tự và acknowledged

LockManager

FeatureMô tả
BackendRedis (Redlock algorithm)
Mục đíchDistributed lock - đảm bảo chỉ một instance thực hiện tác vụ
Ví dụMigration, cron job, upgrade - tránh chạy song song

EventQueue

FeatureMô tả
Mục đíchHàng đợi event nội bộ, xử lý async side-effect
Ví dụGửi notification, sync data, webhook callback

CronManager

FeatureMô tả
Mục đíchLịch chạy tác vụ định kỳ
Multi-instanceKết hợp LockManager để tránh duplicate execution

Logger (@digiforce-nc/logger)

LoggerNội dung log
App loggerApplication lifecycle events
Request loggerMethod, URL, status, duration, request ID
SQL loggerSequelize queries (enable qua config)

Telemetry (@digiforce-nc/telemetry)

OpenTelemetry integration - hook vào lifecycle và request pipeline. Export metrics/traces tới collector.


6) Lifecycle - vòng đời Application

State machine

Chi tiết từng method

load(options) - nạp plugin và chuẩn bị runtime

start(options) - bắt đầu nhận request

stop(options) - tắt server

install(options) - cài đặt mới

upgrade(options) - nâng cấp phiên bản

Thứ tự migration khi upgrade

Core migration luôn chạy trước → preset plugin migration → other plugin migration. Nếu plugin B depend collection của plugin A, migration A phải hoàn thành trước B.

Event hooks - điểm can thiệp cho plugin

EventTimingDùng cho
beforeLoadTrước khi load pluginPre-configure global state
afterLoadSau khi load tất cả pluginKiểm tra dependency, post-configure
beforeStartTrước khi listenWarm cache, check external connections
afterStartĐã listen, nhận requestGhi log, bật cron, notify
beforeStopTrước khi shutdownFlush buffer, cleanup connections
afterStopĐã tắt hoàn toànFinal cleanup
beforeInstallTrước khi install pluginPre-install checks
afterInstallSau khi install xongSeed data, notify admin
beforeLoadPluginTrước khi load từng pluginPer-plugin pre-processing
afterLoadPluginSau khi load từng pluginPer-plugin post-processing

7) Error handling & recovery

Error flow trong pipeline

Recovery patterns

Tình huốngCơ chế recovery
DB connection lostdb.reconnect() - recreate connection pool (trừ SQLite memory)
Plugin crash khi loadPluginManager catch, log error, app vẫn start nhưng plugin đó disabled
Redis disconnectedCacheManager fallback in-memory. PubSub queues retry.
Migration failUmzug track đã chạy. Fix code → chạy lại upgrade → skip migration đã hoàn thành
Uncaught exceptionprocess.on('uncaughtException') → log → graceful shutdown nếu fatal

Không auto-rollback migration

Hệ thống không tự rollback migration fail. Nếu migration lỗi giữa chừng, cần:

  1. Restore DB từ backup.
  2. Fix migration code.
  3. Chạy lại app upgrade.

Luôn backup trước khi upgrade trên production.


8) Walkthrough: truy vết một request

Theo dõi GET /api/users?filter={"status":"active"}&page=1&pageSize=20 qua toàn bộ hệ thống:

Điểm debug quan trọng:

  • Bước 8: Auth fail? → 401. Kiểm tra JWT token, authenticator config.
  • Bước 11: ACL deny? → 403. Kiểm tra role config, snippet, strategy.
  • Bước 12: ACL inject createdById: 1 - user chỉ thấy record mình tạo.
  • Bước 16: SQL query - thấy AND createdById=1 từ fixed params.
  • Bước 17: 0 rows? Filter quá chặt hoặc fixed params conflict.

Đọc tiếp