Files
temp-history-api/api.md
2026-04-21 16:07:17 +07:00

20 KiB

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.jsBackEnd/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:

{
  "status": "success",
  "data": {},
  "message": "OK",
  "errors": []
}

Khi lỗi (HTTP >= 400), envelope:

{
  "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)

{
  "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 idis_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 number bbox min longitude
minLat number bbox min latitude
maxLng number bbox max longitude
maxLat 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)

{
  "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_bylock_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)

{
  "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.idsnapshot.section.title bắt buộc.
  • Mỗi entity/geometry trong snapshot bắt buộc có idoperation 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.
  • 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.