Appearance
Content Versions & Activity
Overview
Hệ thống versions cho phép tạo các snapshot có tên của các content item phục vụ luồng branching và review. Một version lưu trữ một delta (JSON diff) so với item chính và có thể được promote để trở thành nội dung trực tiếp.
Activity log ghi lại toàn bộ các thao tác tạo/cập nhật/xóa trên mọi collection, cung cấp một audit trail đầy đủ.
Versions
Data Model
Một version được gắn với một cặp collection + item cụ thể và có một key dễ đọc (slug). Nhiều version có thể tồn tại đồng thời cho cùng một item.
odp_versions
| Column | Type | Description |
|---|---|---|
id | UUID PK | |
key | string(64) | Slug/tên dễ đọc |
name | string(255) | Tên hiển thị |
collection | string(255) FK → odp_collections | Tên collection |
item | string(255) | Khóa chính của item (dạng chuỗi) |
hash | string(255) | Hash của nội dung delta |
date_created | timestamp | |
date_updated | timestamp | |
user_created | UUID FK → odp_users | |
user_updated | UUID FK → odp_users | |
delta | text (JSON) | Nội dung diff so với item chính |
status | string(20) | Trạng thái review, mặc định draft (xem lifecycle bên dưới) |
Lifecycle của status: draft → pending → approved / rejected.
draft: version mới tạo, editor còn chỉnh sửa được.pending: đã submit cho workflow review (quaPOST /versions/:id/submit).approved: đã được promote thành nội dung trực tiếp.rejected: bị từ chối; không phải terminal — editor có thể submit lại cùng version để mở lại workflow instance.
Endpoints
POST /versions
Tạo một version mới.
Yêu cầu xác thực: Có (quyền được kiểm tra ở service layer)
Request Body:
json
{
"key": "draft-v2",
"name": "Draft Version 2",
"collection": "articles",
"item": "item-uuid",
"delta": {
"title": "Updated Title",
"body": "New body content"
}
}| Field | Type | Required | Description |
|---|---|---|---|
key | string | Có | Slug duy nhất cho version này trong phạm vi (collection, item) |
name | string | Không | Nhãn hiển thị |
collection | string | Có | Tên collection |
item | string | Có | Khóa chính của item |
delta | object | Không | JSON object chứa các field được ghi đè |
Response:
json
{
"data": "new-version-uuid"
}GET /versions
Lấy danh sách version với bộ lọc tùy chọn.
Yêu cầu xác thực: Có
Query Parameters: Các query param chuẩn của ODP (filter, sort, fields, limit, offset, meta).
Ví dụ truy vấn:
GET /versions?filter[collection][_eq]=articles&filter[item][_eq]=article-uuidResponse:
json
{
"data": [
{
"id": "version-uuid",
"key": "draft-v2",
"name": "Draft Version 2",
"collection": "articles",
"item": "article-uuid",
"hash": "abc123...",
"date_created": "2024-01-01T00:00:00.000Z",
"date_updated": null,
"user_created": "user-uuid",
"user_updated": null,
"delta": { "title": "Updated Title" }
}
],
"meta": { "total_count": 1 }
}GET /versions/:id
Đọc một version cụ thể.
Response: Một object version duy nhất.
PATCH /versions/:id
Cập nhật metadata hoặc delta của một version.
Yêu cầu xác thực: Có
**Ownership guard.** Chỉ người tạo version (`user_created`) mới sửa được; admin bypass. Quyền còn được kiểm qua app-permission `versioning:create`.
Request Body:
json
{
"name": "Renamed Draft",
"delta": {
"title": "Even More Updated Title"
}
}Response:
json
{
"data": "version-uuid"
}POST /versions/:id/submit
Submit một version cho workflow review. Đặt status sang pending và phát sự kiện versions.submit.
Yêu cầu xác thực: Có (app-permission versioning:create)
Chỉ người tạo version (hoặc admin) mới submit được. Chỉ version ở trạng thái `draft` hoặc `rejected` mới submit được, và version phải có `delta`.
Response:
json
{
"data": { "id": "version-uuid", "status": "pending" }
}DELETE /versions/:id
Xóa một version (dọn dẹp các draft/rejected cũ).
Yêu cầu xác thực: Có (app-permission versioning:manage)
Không thể xóa version có `status` là `approved` (audit) hoặc `pending` (đang review) — trả về lỗi `InvalidPayload`.
Response: 204 No Content
POST /versions/:id/promote
Promote một version để trở thành nội dung trực tiếp của item cha.
Response: 204 No Content
Business Logic (được triển khai trong VersionsService.promote):
- Đọc bản ghi version bao gồm
delta(toàn bộ tập field cho item mới, hoặc các field đã thay đổi cho lần chỉnh sửa). - Xác định item đích thông qua
version.item(khóa chính mà version liên kết đến). - Tạo mới hoặc cập nhật:
- Nếu item chưa tồn tại →
ItemsService.createOne({ [pk]: version.item, ...delta })— item được tạo khi approval. - Nếu item đã tồn tại →
ItemsService.updateOne(version.item, delta).
- Nếu item chưa tồn tại →
- Thao tác ghi đi qua
ItemsService(không phải cập nhật column trực tiếp), do đó các relational field (o2m / m2m / translations) và nested write được bảo toàn khi promote. - Nếu collection có field
statusvà delta không thiết lập nó, status sẽ được đặt là published. - Đánh dấu version là
approved.
**Không có placeholder item cho nội dung mới.** Việc tạo item mới theo luồng approval không còn chèn một row trống placeholder từ trước nữa. Frontend tự tạo id (uuid PKs) và liên kết draft version với nó; row thực sự chỉ được tạo **khi version được promote**. Các PK auto-increment vẫn fallback về cách tạo placeholder (vì client không thể tự tạo id). Một version bị rejected sẽ không tạo bất kỳ thứ gì.
**Promotion nằm trong transaction của approval.** Khi một workflow promote một version lúc approval cuối cùng, `promote` chạy **bên trong transaction database của approval**. Nếu promotion thất bại, toàn bộ approval sẽ rollback — instance **không** được đánh dấu là hoàn thành và version vẫn ở trạng thái chưa approved (không có trạng thái "hoàn thành nhưng chưa promote"). Xem [[workflow]] → *Approval Outcome & Version Promotion*.
Activity Log
Activity log (odp_activity) ghi lại toàn bộ các thao tác thay đổi dữ liệu trong hệ thống. Đây là log chỉ ghi thêm (append-only) và có thể được truy vấn để xây dựng audit trail.
odp_activity
| Column | Type | Description |
|---|---|---|
id | integer PK (auto-increment) | |
action | string(45) | Loại hành động (xem bên dưới) |
user | UUID FK → odp_users | Người dùng thực hiện hành động |
timestamp | timestamp | Thời điểm thực hiện hành động |
ip | string(50) | IP của client |
user_agent | text | Trình duyệt/client |
collection | string(255) | Collection bị ảnh hưởng |
item | string(255) | Khóa chính của item bị ảnh hưởng |
origin | string(255) | Request origin header |
impersonated_by | UUID | Nếu đang impersonate, đây là user admin thực sự |
Common Action Types
| Action | Description |
|---|---|
create | Item được tạo mới |
update | Item được cập nhật |
delete | Item bị xóa |
login | Người dùng đăng nhập (qua sự kiện auth.login) |
logout | Người dùng đăng xuất |
impersonation.started | Phiên impersonation bắt đầu |
impersonation.stopped | Phiên impersonation kết thúc |
Audit trail của thay đổi do workflow promote — tra "ai" ở đâu
Promote đi qua ItemsService nên có activity + revision, kể cả khi promote do workflow tự chạy lúc approval cuối. Đã kiểm chứng trên dữ liệu thật (Cathay staging, 2026-08-05): archive 1 bài qua luồng duyệt sinh activity action: update + revision với delta = {"status": "archived", …}.
**`activity.user` = `null` với thay đổi do workflow promote.**
WorkflowEngine.promoteVersionInTrx khởi tạo VersionsService với accountability: null (service/api/src/modules/workflow/engine.ts), nên activity row ghi được cái gì và khi nào nhưng không ghi được ai.
Phạm vi ảnh hưởng (đã kiểm 2026-08-05):
odp_activity.user=null.- Field
user-updatedKHÔNG bị null hoá —payload.tschỉ ghi field đó khi cóaccountability.user, nên giá trị cũ được giữ (chỉ là không refresh). - Admin webapp hiện không hiển thị
odp_activityở đâu cả, nên chỗ này chưa lộ ra cho người dùng cuối; nó là vấn đề ở tầng dữ liệu/audit ngoài UI.
"Ai" nằm ở bảng workflow, không ở activity của item:
odp_workflow_instances.started_by— ai gửi yêu cầuodp_workflow_actions.user_id+comment— ai approve/reject, lý doodp_workflow_instance_steps.approvals[].user_id+timestamp
Áp dụng cho mọi thay đổi do workflow promote, không riêng soft-delete. Với soft-delete (archiveField/archiveValue) thì đây là điều cần biết rõ: muốn biết ai xoá 1 bài thì tra màn hình Request/Approval của workflow, không tra activity của bài.
Revisions
Mỗi bản ghi activity có thể có dữ liệu revision liên quan trong odp_revisions:
odp_revisions
| Column | Type | Description |
|---|---|---|
id | integer PK | |
activity | integer FK → odp_activity | Activity cha (CASCADE delete) |
collection | string(255) | Tên collection |
item | string(255) | Khóa chính của item |
data | text (JSON) | Snapshot đầy đủ sau khi thay đổi |
delta | text (JSON) | Chỉ các field đã thay đổi |
parent | integer FK → odp_revisions | Revision cha |
version | UUID FK → odp_versions | Version liên kết (nếu thông qua promote) |
Activity Endpoints
Các endpoint activity chỉ đọc. Xem tài liệu system server để biết danh sách đầy đủ các route CRUD. Activity có thể truy cập tại GET /activity và GET /activity/:id.
Ví dụ: Lấy activity đăng nhập gần đây
GET /activity?filter[action][_eq]=login&sort=-timestamp&limit=20