574 lines
20 KiB
Markdown
574 lines
20 KiB
Markdown
# API - Ultimate History Map (Backend)
|
|
|
|
Tài liệu này mô tả backend API theo đúng hành vi hiện tại của code trong `BackEnd/index.js` và `BackEnd/routes/*`.
|
|
|
|
- Backend: Express
|
|
- Base URL local (hardcode): `http://localhost:3000`
|
|
- DB nghiệp vụ: SQLite `BackEnd/data/polygons.db`
|
|
- Tile data: MBTiles `BackEnd/data/map.mbtiles` (vector) và `BackEnd/data/raster.mbtiles` (raster)
|
|
- Auth/permission: chưa có
|
|
- CORS: mở toàn bộ (`cors()`)
|
|
- Update: 2026-04-21
|
|
|
|
## 1. Envelope Response (Rất Quan Trọng)
|
|
|
|
Mọi endpoint JSON được mount qua `envelopeResponses` sẽ trả về envelope:
|
|
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {},
|
|
"message": "OK",
|
|
"errors": []
|
|
}
|
|
```
|
|
|
|
Khi lỗi (`HTTP >= 400`), envelope:
|
|
|
|
```json
|
|
{
|
|
"status": "error",
|
|
"data": null,
|
|
"message": "Some message",
|
|
"errors": ["Some message"]
|
|
}
|
|
```
|
|
|
|
Quy tắc bọc envelope hiện tại:
|
|
|
|
- Nếu handler `res.json({ error: "..." })` thì `message/errors` sẽ lấy từ `error`.
|
|
- Nếu handler `res.json({ message: "..." })` thì `message/errors` sẽ lấy từ `message`.
|
|
- Nếu handler trả `res.json([...])` hoặc `res.json({ ...domain... })` thì payload nằm trong `data`.
|
|
|
|
Ngoại lệ KHÔNG envelope:
|
|
|
|
- `GET /` trả plain text.
|
|
- `GET /docs` trả Swagger UI (HTML).
|
|
- `GET /docs.json` trả raw OpenAPI JSON (không bọc envelope).
|
|
- Tile binary `GET /tiles/:z/:x/:y`, `GET /raster-tiles/:z/:x/:y` trả binary khi success (vì dùng `res.send`). Các lỗi tile (400/404) vẫn là JSON nên vẫn bị bọc envelope.
|
|
|
|
## 2. Actor (user_id) Và “anonymous”
|
|
|
|
Backend có cơ chế normalize actor:
|
|
|
|
- Đọc actor từ body/query/header tùy endpoint (`user_id`, `user`, `created_by`, `submitted_by`, `reviewed_by`, header `x-user-id`).
|
|
- Hiện tại `normalizeActor()` luôn fallback sang chuỗi `"anonymous"` nếu thiếu.
|
|
|
|
Hệ quả:
|
|
|
|
- Thực tế các field actor **không bắt buộc** (dù code có vài chỗ check `"… is required"` nhưng check đó gần như không bao giờ nổ do luôn fallback `"anonymous"`).
|
|
- Lock/ownership khi thiếu actor sẽ bị “dính” vào cùng một user `"anonymous"` (dễ gây va chạm).
|
|
|
|
Khuyến nghị cho FE/clients: luôn gửi một actor id ổn định (ví dụ user id) để lock và audit có ý nghĩa.
|
|
|
|
## 3. System API
|
|
|
|
### `GET /`
|
|
|
|
Health check.
|
|
|
|
- `200` (text): `GIS server running`
|
|
|
|
### `GET /docs`
|
|
|
|
Swagger UI.
|
|
|
|
### `GET /docs.json`
|
|
|
|
OpenAPI JSON (raw, không envelope).
|
|
|
|
## 4. Tile API (MBTiles)
|
|
|
|
### `GET /tiles/metadata/info`
|
|
|
|
Vector MBTiles metadata từ `map.mbtiles` (table `metadata`).
|
|
|
|
- `200` JSON envelope, trong `data` là object key/value metadata.
|
|
|
|
### `GET /tiles/:z/:x/:y`
|
|
|
|
Vector tile binary (XYZ -> TMS).
|
|
|
|
Luồng xử lý:
|
|
|
|
1. Parse `z/x/y` thành integer; sai -> `400`.
|
|
2. Convert `y` từ XYZ sang TMS: `tmsY = (1 << z) - 1 - y`.
|
|
3. Query `tiles` table theo `(zoom_level, tile_column, tile_row)`.
|
|
4. Không có row -> `404`.
|
|
5. Set header:
|
|
- `Content-Type` dựa trên `metadata.format` (`pbf` -> `application/x-protobuf`).
|
|
- Nếu format `pbf` thì set `Content-Encoding: gzip` (tile data trong MBTiles thường đã gzip sẵn).
|
|
6. `res.send(tile_data)` (binary).
|
|
|
|
### `GET /raster-tiles/metadata/info`
|
|
|
|
Raster MBTiles metadata từ `raster.mbtiles`.
|
|
|
|
- `200` JSON envelope, `data` là metadata object.
|
|
|
|
### `GET /raster-tiles/:z/:x/:y`
|
|
|
|
Raster tile binary (XYZ -> TMS).
|
|
|
|
Luồng xử lý tương tự `/tiles/:z/:x/:y`, khác ở `Content-Type`:
|
|
|
|
- `png` -> `image/png`
|
|
- `jpg|jpeg` -> `image/jpeg`
|
|
- `webp` -> `image/webp`
|
|
- khác -> `application/octet-stream`
|
|
|
|
## 5. Published Entity API (Read-only)
|
|
|
|
Published entities lấy từ table `entities` với filter `is_deleted = 0`.
|
|
|
|
### Entity Object (trong envelope `data`)
|
|
|
|
```json
|
|
{
|
|
"id": "entity-id",
|
|
"name": "Vietnam",
|
|
"slug": "vietnam",
|
|
"description": null,
|
|
"type_id": "country",
|
|
"status": 1,
|
|
"created_at": "2026-04-17T10:00:00.000Z",
|
|
"updated_at": "2026-04-17T10:00:00.000Z",
|
|
"geometry_count": 3
|
|
}
|
|
```
|
|
|
|
### `GET /entities?q=...`
|
|
|
|
List entities, optional search theo `name` hoặc `slug` bằng `LIKE`.
|
|
|
|
Luồng xử lý:
|
|
|
|
1. `q` được trim; nếu rỗng thì list toàn bộ.
|
|
2. Query `entities` + LEFT JOIN `entity_geometries` để tính `geometry_count`.
|
|
3. Sort theo `name COLLATE NOCASE ASC`.
|
|
|
|
### `GET /entities/search?name=...&limit=...`
|
|
|
|
Search entity theo `name` bằng `LIKE`.
|
|
|
|
Luồng xử lý:
|
|
|
|
1. `name` rỗng -> trả `[]`.
|
|
2. `limit` default `25`, max `100`.
|
|
3. Query `entities` + LEFT JOIN `entity_geometries` để tính `geometry_count`.
|
|
|
|
### `GET /entities/:id`
|
|
|
|
Luồng xử lý:
|
|
|
|
1. Query entity theo `id` và `is_deleted = 0`.
|
|
2. Không tồn tại -> `404` (`error: "Entity not found"`).
|
|
|
|
## 6. Published Geometry API (Read-only)
|
|
|
|
Published geometries lấy từ table `geometries` với filter `is_deleted = 0`.
|
|
|
|
### `GET /geometries`
|
|
|
|
Query geometries theo bbox/time/entity.
|
|
|
|
Query params:
|
|
|
|
| Field | Bắt buộc | Type | Ghi chú |
|
|
| --- | ---: | --- | --- |
|
|
| `minLng` | Có | number | bbox min longitude |
|
|
| `minLat` | Có | number | bbox min latitude |
|
|
| `maxLng` | Có | number | bbox max longitude |
|
|
| `maxLat` | Có | number | bbox max latitude |
|
|
| `time` | Không | number | parse int; filter temporal range |
|
|
| `entity_id` | Không | string | lọc geometry có link entity này |
|
|
|
|
Luồng xử lý:
|
|
|
|
1. Validate bbox là số hữu hạn; fail -> `400` (`Missing/invalid bbox`).
|
|
2. Nếu có `time` (không rỗng) thì parse số; fail -> `400` (`Invalid time`).
|
|
3. Query geometries theo:
|
|
- bbox intersects
|
|
- nếu có `time` thì pass khi:
|
|
- `time_start` null hoặc `time_start <= time`
|
|
- `time_end` null hoặc `time_end >= time`
|
|
- nếu có `entity_id` thì dùng `EXISTS` trên `entity_geometries` + `entities.is_deleted = 0`
|
|
4. Load links entity cho các geometry id trả về, giữ thứ tự theo `entity_geometries.rowid ASC`.
|
|
5. Build GeoJSON FeatureCollection:
|
|
- `feature.geometry` parse từ `geometries.draw_geometry` (JSON string).
|
|
- `properties.binding` được parse/normalize từ `geometries.binding` (JSON string hoặc fallback).
|
|
- `properties.entity_ids/entity_names/...` dựa trên link table.
|
|
- `properties.type`:
|
|
- ưu tiên `geometries.type` nếu khác null và không phải legacy `line|path`
|
|
- nếu legacy `line|path` thì fallback sang `entities.type_id` của primary entity.
|
|
|
|
### Geometry Feature (trong envelope `data`)
|
|
|
|
```json
|
|
{
|
|
"type": "Feature",
|
|
"properties": {
|
|
"id": "geometry-id",
|
|
"type": "country",
|
|
"time_start": 1802,
|
|
"time_end": 1884,
|
|
"binding": [],
|
|
"entity_id": "entity-id",
|
|
"entity_ids": ["entity-id"],
|
|
"entity_name": "Vietnam",
|
|
"entity_names": ["Vietnam"],
|
|
"entity_type_id": "country"
|
|
},
|
|
"geometry": { "type": "Polygon", "coordinates": [] }
|
|
}
|
|
```
|
|
|
|
## 7. Section Workflow API (Write path chính)
|
|
|
|
Không có API mutate trực tiếp published `entities/geometries`. Mọi thay đổi đi qua section workflow:
|
|
|
|
1. Editor mở section (`GET /sections/:id/editor`) để lấy snapshot và lock.
|
|
2. Editor commit snapshot (`POST /sections/:id/commits` hoặc `/restore`).
|
|
3. Editor submit head (`POST /sections/:id/submit`) -> tạo submission pending.
|
|
4. Reviewer approve/reject (`POST /submissions/:id/...`).
|
|
5. Chỉ approve mới apply snapshot vào published tables.
|
|
|
|
### 7.1. Data Model (khái niệm)
|
|
|
|
- `sections`: metadata của draft space.
|
|
- `section_states`: trạng thái và lock, version, head commit.
|
|
- `section_commits`: lịch sử snapshot (không apply).
|
|
- `section_submissions`: snapshot pending/review.
|
|
|
|
Status:
|
|
|
|
- `section_states.status`: `editing | submitted | approved | rejected`
|
|
- `section_submissions.status`: `pending | approved | rejected | conflicted`
|
|
|
|
Lock:
|
|
|
|
- TTL 15 phút (`LOCK_TTL_MS = 15 * 60 * 1000`).
|
|
- Lock được check theo `locked_by` và `lock_expires_at`.
|
|
|
|
Optimistic concurrency:
|
|
|
|
- Client có thể gửi `expected_version` và/hoặc `expected_head_commit_id` khi commit/restore.
|
|
- Sai -> `409` (`Section version changed` / `Section head commit changed`).
|
|
|
|
### 7.2. `GET /sections`
|
|
|
|
List sections + state (LEFT JOIN). Sort `sections.updated_at DESC`.
|
|
|
|
### 7.3. `POST /sections`
|
|
|
|
Tạo section và tạo luôn state (status `editing`, version `0`).
|
|
|
|
Luồng xử lý:
|
|
|
|
1. Validate `title` là string non-empty (trim). Sai -> `400` (`title is required`).
|
|
2. `id` nếu thiếu thì server tự sinh UUID.
|
|
3. Insert vào `sections`.
|
|
4. `ensureSectionState(section_id)` (insert `section_states` nếu chưa có).
|
|
5. SQLite constraint (trùng id) -> `409` (`Section id already exists`).
|
|
|
|
### 7.4. `GET /sections/:sectionId/editor`
|
|
|
|
Mở editor và (có side-effect) acquire lock.
|
|
|
|
Actor lấy từ query/header:
|
|
|
|
- `?user_id=...` hoặc `?user=...`
|
|
- header `x-user-id`
|
|
|
|
Luồng xử lý (transaction):
|
|
|
|
1. Load section; không có -> `404` (`Section not found`).
|
|
2. `ensureSectionState`.
|
|
3. `assertLockAvailable(state, actor, now)`:
|
|
- OK nếu chưa lock, hoặc lock bởi chính actor, hoặc lock đã expire.
|
|
- Fail -> `409` (`Section is locked by another user`).
|
|
4. Acquire/refresh lock cho actor (set `locked_by/locked_at/lock_expires_at`).
|
|
5. Nếu có `head_commit_id` thì load commit đó.
|
|
6. Response:
|
|
- `section`: object normalized
|
|
- `state`: object normalized
|
|
- `commit`: normalized commit (không include snapshot)
|
|
- `snapshot`:
|
|
- nếu có head commit: parse `snapshot_json`
|
|
- nếu chưa có commit: trả empty snapshot `{ schema_version, section, entities:[], geometries:[], link_scopes:[] }`
|
|
|
|
Ghi chú: vì actor fallback `"anonymous"`, gọi endpoint này mà không gửi user id vẫn tạo lock (locked_by = `"anonymous"`).
|
|
|
|
### 7.5. `POST /sections/:sectionId/lock`
|
|
|
|
Acquire/refresh lock (tách riêng, không trả snapshot).
|
|
|
|
Luồng xử lý:
|
|
|
|
1. Load section; không có -> `404`.
|
|
2. `ensureSectionState`.
|
|
3. `assertLockAvailable`.
|
|
4. Acquire lock và trả `state`.
|
|
|
|
### 7.6. `POST /sections/:sectionId/unlock`
|
|
|
|
Release lock.
|
|
|
|
Luồng xử lý:
|
|
|
|
1. Load section; không có -> `404`.
|
|
2. `ensureSectionState`.
|
|
3. Nếu `state.locked_by` tồn tại và khác actor hiện tại -> `409` (`Section is locked by another user`).
|
|
4. Clear lock fields.
|
|
|
|
### 7.7. `GET /sections/:sectionId/commits?include_snapshot=1`
|
|
|
|
List commit history (sort `commit_no DESC`).
|
|
|
|
- `include_snapshot=1|true` -> mỗi commit có thêm `snapshot` (parsed JSON).
|
|
|
|
### 7.8. `POST /sections/:sectionId/commits`
|
|
|
|
Tạo commit mới từ snapshot editor.
|
|
|
|
Body:
|
|
|
|
- `snapshot` (object) hoặc `snapshot_json` (string JSON) là bắt buộc.
|
|
- Actor thường gửi qua `created_by` hoặc `user_id` hoặc header `x-user-id`.
|
|
- Optional:
|
|
- `expected_version`
|
|
- `expected_head_commit_id`
|
|
- `title`, `note`
|
|
|
|
Luồng xử lý (transaction):
|
|
|
|
1. Load section; không có -> `404`.
|
|
2. Normalize snapshot input:
|
|
- `snapshot_json` không parse được -> `400` (`snapshot_json must be valid JSON`)
|
|
- thiếu snapshot -> `400` (`snapshot is required`)
|
|
3. `ensureSectionState`.
|
|
4. `assertCanEdit`:
|
|
- status phải là `editing` hoặc `rejected`, không thì `409` (`Section is not editable`)
|
|
- lock phải available, không thì `409` (`Section is locked by another user`)
|
|
5. `assertExpectedState` nếu client có gửi expected fields.
|
|
6. `validateSnapshot`:
|
|
- contract level (bắt buộc có `snapshot.section.id/title`, entity/geometry ids, link_scope shape)
|
|
- rule bổ sung: name required (trừ delete/reference), slug không trùng (case-insensitive), time range hợp lệ, bbox nếu có phải hợp lệ, binding không chứa self id, link_scope entity_ids không rỗng.
|
|
7. Insert `section_commits`:
|
|
- `parent_commit_id = state.head_commit_id`
|
|
- `commit_no` auto tăng
|
|
- `snapshot_hash = sha256(JSON)`
|
|
8. Update `section_states`:
|
|
- `status = 'editing'`
|
|
- `head_commit_id = newCommitId`
|
|
- `version = version + 1`
|
|
9. Update `sections.updated_at`.
|
|
|
|
Response `201`: `{ commit, state }` (commit có `snapshot`).
|
|
|
|
### 7.9. `POST /sections/:sectionId/restore`
|
|
|
|
Restore một commit cũ bằng cách tạo commit mới `kind = "restore"`.
|
|
|
|
Luồng xử lý giống `/commits` nhưng snapshot lấy từ commit nguồn:
|
|
|
|
1. Validate `commit_id`/`restore_commit_id`; thiếu -> `400` (`commit_id is required`).
|
|
2. Check commit nguồn thuộc section; không có -> `404` (`Commit not found`).
|
|
3. Insert commit mới với `restored_from_commit_id = source.id`.
|
|
4. Update head commit + bump version.
|
|
|
|
### 7.10. `POST /sections/:sectionId/submit`
|
|
|
|
Submit head commit hiện tại -> tạo submission pending, set state `submitted`, release lock.
|
|
|
|
Luồng xử lý (transaction):
|
|
|
|
1. Load section; không có -> `404`.
|
|
2. `ensureSectionState`.
|
|
3. `assertLockAvailable` (chỉ check không bị user khác lock).
|
|
4. status phải là `editing`, không thì `409` (`Section is not editable`).
|
|
5. `head_commit_id` phải có, không thì `400` (`Section has no head commit to submit`).
|
|
6. Load commit head; không có -> `404` (`Commit not found`).
|
|
7. Insert `section_submissions` (status `pending`) copy snapshot từ commit head.
|
|
8. Update `section_states`:
|
|
- `status = 'submitted'`
|
|
- clear lock fields.
|
|
|
|
Response `201`: `SectionSubmission` (có `snapshot`).
|
|
|
|
### 7.11. `GET /sections/:sectionId/submissions?include_snapshot=1`
|
|
|
|
List submission history của section (sort `submitted_at DESC`).
|
|
|
|
## 8. Review API (`/submissions`)
|
|
|
|
Review endpoints được mount “public” qua `/submissions` nhưng implementation nằm trong `BackEnd/routes/sections.js`:
|
|
|
|
- `POST /submissions/:submissionId/approve`
|
|
- `POST /submissions/:submissionId/reject`
|
|
|
|
### 8.1. `POST /submissions/:submissionId/reject`
|
|
|
|
Luồng xử lý (transaction):
|
|
|
|
1. Load submission; không có -> `404` (`Submission not found`).
|
|
2. Submission phải `pending`, không thì `409` (`Submission is not pending`).
|
|
3. Update submission:
|
|
- `status = rejected`
|
|
- set `reviewed_by/reviewed_at/review_note`
|
|
4. Update `section_states.status = rejected`.
|
|
5. Không apply published tables.
|
|
|
|
Response `200`: `SectionSubmission` (có `snapshot`).
|
|
|
|
### 8.2. `POST /submissions/:submissionId/approve`
|
|
|
|
Luồng xử lý (transaction):
|
|
|
|
1. Load submission; không có -> `404`.
|
|
2. Submission phải `pending`, không thì `409`.
|
|
3. Verify snapshot hash:
|
|
- nếu `submission.snapshot_hash` tồn tại và khác `sha256(submission.snapshot_json)` -> `409` (`Submission snapshot hash mismatch`).
|
|
4. Parse snapshot JSON.
|
|
5. `applySnapshotToPublished(snapshot, now)` theo thứ tự:
|
|
- apply `entities`
|
|
- apply `geometries`
|
|
- apply `link_scopes` (replace links)
|
|
6. Update submission:
|
|
- `status = approved`
|
|
- set `reviewed_by/reviewed_at/review_note`
|
|
7. Update `section_states.status = approved`.
|
|
|
|
Conflict handling đặc biệt:
|
|
|
|
- Nếu trong lúc apply snapshot phát hiện conflict (409) thì backend:
|
|
- (best-effort) update `section_submissions.status = conflicted` và set `review_note = <conflict message>` nếu submission vẫn còn `pending`.
|
|
- trả `409` cho client.
|
|
|
|
## 9. Snapshot Format Và Apply Semantics
|
|
|
|
Commit/submission snapshot là full JSON object. Backend quan tâm các mảng:
|
|
|
|
- `entities[]`
|
|
- `geometries[]`
|
|
- `link_scopes[]`
|
|
|
|
Ngoài ra FE có thể kèm `editor_feature_collection` để preview (backend chỉ normalize, không apply vào DB).
|
|
|
|
### 9.1. Snapshot Shape (rút gọn)
|
|
|
|
```json
|
|
{
|
|
"schema_version": 1,
|
|
"section": { "id": "section-id", "title": "Section title" },
|
|
"editor_feature_collection": { "type": "FeatureCollection", "features": [] },
|
|
"entities": [],
|
|
"geometries": [],
|
|
"link_scopes": []
|
|
}
|
|
```
|
|
|
|
Operations (case-insensitive):
|
|
|
|
- `create | update | delete | reference | replace`
|
|
- `replace` được normalize thành `update` cho entity/geometry.
|
|
|
|
### 9.2. Validate Snapshot (400)
|
|
|
|
Backend validate ở `/commits` và trước khi approve:
|
|
|
|
- `snapshot.section.id` và `snapshot.section.title` bắt buộc.
|
|
- Mỗi entity/geometry trong snapshot bắt buộc có `id` và `operation` hợp lệ.
|
|
- Entity:
|
|
- `name` bắt buộc nếu không phải `delete|reference`
|
|
- `slug` (nếu có) không được trùng trong cùng snapshot (case-insensitive)
|
|
- Geometry:
|
|
- nếu `create|update|replace` thì bắt buộc có `draw_geometry` hoặc `geometry`
|
|
- nếu có cả `time_start/time_end` thì `time_start <= time_end`
|
|
- `bbox` nếu gửi thì phải hợp lệ; nếu không gửi, approve sẽ tự tính bbox từ geometry
|
|
- `binding` không được chứa chính `geometry.id`
|
|
- Link scope:
|
|
- `geometry_id` bắt buộc, `entity_ids` phải là array
|
|
- `entity_ids` sau normalize không được rỗng
|
|
|
|
### 9.3. Apply Snapshot (Approve)
|
|
|
|
#### Entities
|
|
|
|
- `create`: INSERT `entities` (type_id default `"country"` nếu thiếu), `created_at/updated_at = now`.
|
|
- `update|replace`: UPDATE toàn bộ fields; có thể check conflict bằng `base_updated_at` và/hoặc `base_hash` (nếu snapshot item gửi).
|
|
- `delete`: set `entities.is_deleted = 1`, xóa mọi link trong `entity_geometries`.
|
|
- `reference`: bỏ qua.
|
|
|
|
#### Geometries
|
|
|
|
- `create`: INSERT `geometries`:
|
|
- `draw_geometry` được stringify JSON
|
|
- `bbox`: nếu snapshot có `bbox` hợp lệ thì dùng, nếu không thì compute bằng `getBBox(draw_geometry)`
|
|
- `binding`: normalize array id và tự loại self id
|
|
- `update|replace`: UPDATE toàn bộ fields tương tự create (có check conflict bằng base fields).
|
|
- `delete`:
|
|
- set `geometries.is_deleted = 1`
|
|
- delete links trong `entity_geometries` theo `geometry_id`
|
|
- remove mọi reference tới geometry đó trong `binding` của các geometry khác.
|
|
- `reference`: bỏ qua.
|
|
|
|
#### Link scopes (replace entity links theo geometry)
|
|
|
|
- Geometry phải tồn tại và `is_deleted = 0`, nếu không -> conflict (409).
|
|
- `entity_ids` không rỗng, và mọi entity id phải tồn tại + `is_deleted = 0`, nếu không -> `400`.
|
|
- Nếu snapshot gửi `base_links_hash`:
|
|
- backend hash current links (sorted by entity_id) và so sánh; mismatch -> conflict (409).
|
|
- Apply bằng cách:
|
|
- `DELETE FROM entity_geometries WHERE geometry_id = ?`
|
|
- rồi `INSERT` lại toàn bộ link theo thứ tự `entity_ids` gửi lên.
|
|
|
|
### 9.4. Conflict Semantics (409)
|
|
|
|
Nhóm lỗi conflict (status 409, `err.isConflict = true`) xảy ra khi approve apply:
|
|
|
|
- Update/delete entity/geometry nhưng record không tồn tại.
|
|
- `base_updated_at` mismatch hoặc `base_hash` mismatch.
|
|
- Link scope geometry không tồn tại/đã deleted.
|
|
- `base_links_hash` mismatch.
|
|
|
|
## 10. Status Code Summary
|
|
|
|
| Status | Khi nào |
|
|
| --- | --- |
|
|
| `200` | OK (read/review) |
|
|
| `201` | Created (section/commit/restore/submit) |
|
|
| `400` | Query/body/snapshot không hợp lệ |
|
|
| `404` | Resource không tồn tại |
|
|
| `409` | Lock conflict, stale version/head, state không editable, conflict khi apply, hoặc DB constraint |
|
|
| `500` | Lỗi server |
|
|
|
|
## 11. Luồng Tham Chiếu (End-to-End)
|
|
|
|
### 11.1. Editor Flow
|
|
|
|
1. List section: `GET /sections`.
|
|
2. Open editor (và lock): `GET /sections/:sectionId/editor?user_id=<actor>`.
|
|
3. User edit local -> tạo snapshot.
|
|
4. Commit: `POST /sections/:sectionId/commits` (khuyến nghị gửi `expected_version` + `expected_head_commit_id` từ state).
|
|
5. (Optional) Restore: `POST /sections/:sectionId/restore`.
|
|
6. Submit: `POST /sections/:sectionId/submit`.
|
|
|
|
### 11.2. Reviewer Flow
|
|
|
|
1. List sections: `GET /sections`.
|
|
2. Với từng section: `GET /sections/:sectionId/submissions?include_snapshot=1`.
|
|
3. Approve/reject:
|
|
- `POST /submissions/:submissionId/approve`
|
|
- `POST /submissions/:submissionId/reject`
|
|
|
|
## 12. Giới Hạn / “Gotchas” Hiện Tại
|
|
|
|
- Không có auth/permission.
|
|
- Actor fallback `"anonymous"` làm “required actor” không thực sự enforced; lock và audit dễ sai nếu client không gửi user id.
|
|
- `GET /sections/:id/editor` luôn acquire lock (side-effect). Nếu bạn chỉ muốn đọc snapshot mà không lock, hiện chưa có endpoint riêng.
|
|
- Không có pagination cho list sections/commits/submissions.
|
|
- Reviewer UI nếu cần list submissions toàn cục: hiện chưa có endpoint `GET /submissions` (phải gom theo section).
|
|
- Conflict protection (`base_hash/base_updated_at/base_links_hash`) chỉ hiệu quả nếu FE gửi các field này.
|
|
|