# 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.

