Giao diện
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 model và Request 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
| Layer | Service | Phụ thuộc | Ghi chú |
|---|---|---|---|
| Data | DataSourceManager | - | Quản lý tất cả data source (main + external) |
MainDataSource | Database, ACL, ResourceManager | Data source mặc định, tạo trong init() | |
Database | Sequelize | ORM layer, quản lý collection/model/repository | |
| API | ResourceManager | - | Parse URL → Resource → Action, middleware pipeline |
| Action handlers | ResourceManager | list, get, create, update, destroy, query | |
| Security | AuthManager | JwtService | Xác thực request, quản lý auth types |
ACL | - | Engine phân quyền runtime (in-memory) | |
| Plugin | PluginManager | Application | Load, install, enable/disable plugin |
CLI | PluginManager | CLI commands cho vận hành | |
ApplicationVersion | Database | Track version app + plugins | |
| Infra | CacheManager | Redis (optional) | Cache API, wrap pattern |
PubSubManager | Redis | Cross-instance messaging | |
LockManager | Redis | Distributed 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ước | Tại sao phải ở vị trí này |
|---|---|
| 1. Logger | Tất cả bước sau cần log. Phải dựng đầu tiên. |
| 2. MainDataSource | Tạo DB, ResourceManager, ACL. Mọi service sau đều cần ít nhất một trong ba. |
| 3. Service nền | Redis/pubsub cần cho cache manager và lock. Worker ID cần cho snowflake field. |
| 4. PluginManager | Cần DB (từ bước 2) để đọc danh sách plugin đã cài. |
| 5. AuthManager | Cần ResourceManager (từ bước 2) để define resource auth. |
| 6. Hooks | Cần PluginManager + ACL đã có. Hook sync availableActions vào ACL khi data source mới được add. |
| 7. DS middleware | Cần AuthManager (bước 5) để gắn auth middleware vào pipeline. |
| 8. App middleware | Gắn sau DS middleware vì middleware dataSource (tầng 1) phải biết DataSourceManager đã setup. |
| 9. Actions | Register sau middleware vì handler cần middleware chain đã đầy đủ. |
| 10. CLI | Cuố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 app | Tầng 2 - Data source pipeline | |
|---|---|---|
| Scope | Mọi HTTP request | Request tới resource/action |
| Quan tâm | Parse body, CORS, logging, format response | Auth, ACL, data operation |
| Đăng ký bằng | app.use(fn, topoTags) | dataSourceManager.use(fn) hoặc resourceManager.use(fn) |
| Có per-source? | Không - chung cho mọi request | Có - mỗi DataSource có pipeline riêng |
| Auth/ACL | Không | Có - 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:
| Tag | Vị trí Toposort | Chức năng | Ghi chú |
|---|---|---|---|
generateReqId | Đầu tiên | Gắn UUID vào ctx.reqId và header X-Request-Id | Tất cả log sau đều kèm ID này |
audit | after generateReqId | Ghi sự kiện kiểm toán (ai làm gì) | Tùy cấu hình audit plugin |
logger | after audit | Log request: method, URL, status, duration | Tích hợp @digiforce-nc/logger |
bodyParser | after logger | Parse application/json và multipart/form-data | Có thể tắt cho route cụ thể |
| (helper) | after bodyParser | Gắn ctx.getBearerToken() utility | Không phải middleware riêng, gắn kèm |
i18n | before cors | Detect locale từ header/query, gắn ctx.i18n | Ảnh hưởng error message ngôn ngữ |
cors | after bodyParser | CORS headers, preflight handling | Config từ env hoặc app options |
extractClientIp | before cors | Lấy IP thực từ X-Forwarded-For / X-Real-IP | Cần khi app sau proxy/LB |
dataWrapping | after cors | Wrap response thành { data, meta } format | Có thể tắt (options.dataWrapping = false) |
dataSource | after dataWrapping | Chọn data source → delegate sang tầng 2 | Middleware 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 đó.
| Middleware | Tag Toposort | Chức năng chi tiết |
|---|---|---|
validate-filter-params | Đầu pipeline | Kiểm tra và chuẩn hoá filter input - chặn filter injection sớm |
auth | after validate | AuthManager.middleware() - verify JWT, resolve user, gắn ctx.state.currentUser + ctx.state.currentRole |
| ResourceManager | after auth | Parse URL path → resolve Resource + Action. Gắn ctx.action với actionName, resourceName, params |
acl | after ResourceManager | ACL.middleware() - kiểm tra quyền, merge fixed params (filter bổ sung theo role) |
parseVariables | after acl | Thay thế biến template (như {{ $user.id }}) trong params bằng giá trị thực |
dataTemplate | after acl | Áp dụng data template cho action |
| Pre-action handlers | after dataTemplate | Plugin đăng ký qua registerPreActionHandler - validate input, inject logic |
| Action handler | Cuối | Handler 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ả:
- Response đi ngược qua ACL (có thể filter response fields).
- Đi ngược qua
dataWrapping(wrap response format). - Đ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:exportResource 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 pattern | HTTP Method | Action | Ví dụ |
|---|---|---|---|
/:resource | GET | list | GET /api/users |
/:resource/:id | GET | get | GET /api/users/1 |
/:resource | POST | create | POST /api/users |
/:resource/:id | PUT / PATCH | update | PUT /api/users/1 |
/:resource/:id | DELETE | destroy | DELETE /api/users/1 |
/:assoc/:assocId/:resource | GET | list (association) | GET /api/users/1/orders |
/:assoc/:assocId/:resource/:id | GET | get (association) | GET /api/users/1/orders/5 |
/:assoc/:assocId/:resource | POST | create / set / add | POST /api/users/1/roles |
/:assoc/:assocId/:resource/:id | DELETE | destroy / remove | DELETE /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ằng | Scope | Ví dụ |
|---|---|---|---|
| Global middleware | resourceManager.use(fn, topo) | Mọi resource | Logging, rate limit |
| Resource middleware | resource.use(fn) | Resource cụ thể | Custom header check |
| Action middleware | action.use(fn) | Action cụ thể | File upload handler |
| Pre-action | registerPreActionHandler(name, fn, topo) | Theo action name | Validate, inject filter |
| Handler | registerActionHandler(name, fn) | Theo action name | CRUD 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ù:
| Param | Strategy | Lý do |
|---|---|---|
filter | and-merge | Giữ mọi filter (bảo mật + nghiệp vụ), không cho ghi đè |
fields / whitelist / blacklist | intersect | Chỉ giữ field nằm trong tất cả danh sách |
appends / except | union | Gộp relation cần eager load |
sort | overwrite | Chỉ 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 là đơn vị độc lập với bộ ba: CollectionManager + ResourceManager + ACL. Nghĩa là:
- Collection
userstrênmainvàuserstrênexternal-pghoàn toàn khác nhau (khác schema, khác quyền). - ACL policy trên
mainkhông áp dụng choexternal-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:
- URL parse được ra resource hợp lệ.
- Collection tồn tại trong CollectionManager.
- 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:
| Feature | Mô tả |
|---|---|
collectionsFilter | Chỉ 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)
| Feature | Mô tả |
|---|---|
| Backend | In-memory (single instance) hoặc Redis (multi-instance) |
| API | get, set, del, wrap (cache-aside pattern) |
| Namespace | Hỗ trợ namespace cho từng plugin |
| TTL | Configurable per key |
PubSubManager
| Feature | Mô tả |
|---|---|
| Backend | Redis pub/sub |
| Mục đích | Broadcast event giữa instances: plugin state change, cache invalidation |
| API | publish(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
| Feature | Mô tả |
|---|---|
| Xây trên | PubSubManager |
| 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
| Feature | Mô tả |
|---|---|
| Backend | Redis (Redlock algorithm) |
| Mục đích | Distributed 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
| Feature | Mô tả |
|---|---|
| Mục đích | Hàng đợi event nội bộ, xử lý async side-effect |
| Ví dụ | Gửi notification, sync data, webhook callback |
CronManager
| Feature | Mô tả |
|---|---|
| Mục đích | Lịch chạy tác vụ định kỳ |
| Multi-instance | Kết hợp LockManager để tránh duplicate execution |
Logger (@digiforce-nc/logger)
| Logger | Nội dung log |
|---|---|
| App logger | Application lifecycle events |
| Request logger | Method, URL, status, duration, request ID |
| SQL logger | Sequelize 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
| Event | Timing | Dùng cho |
|---|---|---|
beforeLoad | Trước khi load plugin | Pre-configure global state |
afterLoad | Sau khi load tất cả plugin | Kiểm tra dependency, post-configure |
beforeStart | Trước khi listen | Warm cache, check external connections |
afterStart | Đã listen, nhận request | Ghi log, bật cron, notify |
beforeStop | Trước khi shutdown | Flush buffer, cleanup connections |
afterStop | Đã tắt hoàn toàn | Final cleanup |
beforeInstall | Trước khi install plugin | Pre-install checks |
afterInstall | Sau khi install xong | Seed data, notify admin |
beforeLoadPlugin | Trước khi load từng plugin | Per-plugin pre-processing |
afterLoadPlugin | Sau khi load từng plugin | Per-plugin post-processing |
7) Error handling & recovery
Error flow trong pipeline
Recovery patterns
| Tình huống | Cơ chế recovery |
|---|---|
| DB connection lost | db.reconnect() - recreate connection pool (trừ SQLite memory) |
| Plugin crash khi load | PluginManager catch, log error, app vẫn start nhưng plugin đó disabled |
| Redis disconnected | CacheManager fallback in-memory. PubSub queues retry. |
| Migration fail | Umzug track đã chạy. Fix code → chạy lại upgrade → skip migration đã hoàn thành |
| Uncaught exception | process.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:
- Restore DB từ backup.
- Fix migration code.
- 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=1từ fixed params. - Bước 17: 0 rows? Filter quá chặt hoặc fixed params conflict.
Đọc tiếp
- Tổng quan hệ thống - system context, deployment
- Client architecture - SPA React tương ứng
- Data model & metadata - Collection, Field, UI Schema
- Security model - Auth + ACL deep dive
- Request lifecycle - sequence diagram cho write, WS, install, upgrade
- Core: Server Application - deep dive code-level
- Core: Resourcer & Actions - ResourceManager internals
- Core: DataSourceManager - multi data source internals
- FAQ - câu hỏi thường gặp