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.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:
{
"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/errorssẽ lấy từerror. - Nếu handler
res.json({ message: "..." })thìmessage/errorssẽ lấy từmessage. - Nếu handler trả
res.json([...])hoặcres.json({ ...domain... })thì payload nằm trongdata.
Ngoại lệ KHÔNG envelope:
GET /trả plain text.GET /docstrả Swagger UI (HTML).GET /docs.jsontrả raw OpenAPI JSON (không bọc envelope).- Tile binary
GET /tiles/:z/:x/:y,GET /raster-tiles/:z/:x/:ytrả binary khi success (vì dùngres.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, headerx-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).
200JSON envelope, trongdatalà object key/value metadata.
GET /tiles/:z/:x/:y
Vector tile binary (XYZ -> TMS).
Luồng xử lý:
- Parse
z/x/ythành integer; sai ->400. - Convert
ytừ XYZ sang TMS:tmsY = (1 << z) - 1 - y. - Query
tilestable theo(zoom_level, tile_column, tile_row). - Không có row ->
404. - Set header:
Content-Typedựa trênmetadata.format(pbf->application/x-protobuf).- Nếu format
pbfthì setContent-Encoding: gzip(tile data trong MBTiles thường đã gzip sẵn).
res.send(tile_data)(binary).
GET /raster-tiles/metadata/info
Raster MBTiles metadata từ raster.mbtiles.
200JSON envelope,datalà 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/pngjpg|jpeg->image/jpegwebp->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ý:
qđược trim; nếu rỗng thì list toàn bộ.- Query
entities+ LEFT JOINentity_geometriesđể tínhgeometry_count. - Sort theo
name COLLATE NOCASE ASC.
GET /entities/search?name=...&limit=...
Search entity theo name bằng LIKE.
Luồng xử lý:
namerỗng -> trả[].limitdefault25, max100.- Query
entities+ LEFT JOINentity_geometriesđể tínhgeometry_count.
GET /entities/:id
Luồng xử lý:
- Query entity theo
idvàis_deleted = 0. - 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ý:
- Validate bbox là số hữu hạn; fail ->
400(Missing/invalid bbox). - Nếu có
time(không rỗng) thì parse số; fail ->400(Invalid time). - Query geometries theo:
- bbox intersects
- nếu có
timethì pass khi:time_startnull hoặctime_start <= timetime_endnull hoặctime_end >= time
- nếu có
entity_idthì dùngEXISTStrênentity_geometries+entities.is_deleted = 0
- Load links entity cho các geometry id trả về, giữ thứ tự theo
entity_geometries.rowid ASC. - Build GeoJSON FeatureCollection:
feature.geometryparse 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.typenếu khác null và không phải legacyline|path - nếu legacy
line|paththì fallback sangentities.type_idcủa primary entity.
- ưu tiên
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:
- Editor mở section (
GET /sections/:id/editor) để lấy snapshot và lock. - Editor commit snapshot (
POST /sections/:id/commitshoặc/restore). - Editor submit head (
POST /sections/:id/submit) -> tạo submission pending. - Reviewer approve/reject (
POST /submissions/:id/...). - 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 | rejectedsection_submissions.status:pending | approved | rejected | conflicted
Lock:
- TTL 15 phút (
LOCK_TTL_MS = 15 * 60 * 1000). - Lock được check theo
locked_byvàlock_expires_at.
Optimistic concurrency:
- Client có thể gửi
expected_versionvà/hoặcexpected_head_commit_idkhi 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ý:
- Validate
titlelà string non-empty (trim). Sai ->400(title is required). idnếu thiếu thì server tự sinh UUID.- Insert vào
sections. ensureSectionState(section_id)(insertsection_statesnếu chưa có).- 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):
- Load section; không có ->
404(Section not found). ensureSectionState.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).
- Acquire/refresh lock cho actor (set
locked_by/locked_at/lock_expires_at). - Nếu có
head_commit_idthì load commit đó. - Response:
section: object normalizedstate: object normalizedcommit: 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:[] }
- nếu có head commit: parse
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ý:
- Load section; không có ->
404. ensureSectionState.assertLockAvailable.- Acquire lock và trả
state.
7.6. POST /sections/:sectionId/unlock
Release lock.
Luồng xử lý:
- Load section; không có ->
404. ensureSectionState.- Nếu
state.locked_bytồn tại và khác actor hiện tại ->409(Section is locked by another user). - 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êmsnapshot(parsed JSON).
7.8. POST /sections/:sectionId/commits
Tạo commit mới từ snapshot editor.
Body:
snapshot(object) hoặcsnapshot_json(string JSON) là bắt buộc.- Actor thường gửi qua
created_byhoặcuser_idhoặc headerx-user-id. - Optional:
expected_versionexpected_head_commit_idtitle,note
Luồng xử lý (transaction):
- Load section; không có ->
404. - Normalize snapshot input:
snapshot_jsonkhông parse được ->400(snapshot_json must be valid JSON)- thiếu snapshot ->
400(snapshot is required)
ensureSectionState.assertCanEdit:- status phải là
editinghoặcrejected, không thì409(Section is not editable) - lock phải available, không thì
409(Section is locked by another user)
- status phải là
assertExpectedStatenếu client có gửi expected fields.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.
- contract level (bắt buộc có
- Insert
section_commits:parent_commit_id = state.head_commit_idcommit_noauto tăngsnapshot_hash = sha256(JSON)
- Update
section_states:status = 'editing'head_commit_id = newCommitIdversion = version + 1
- 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:
- Validate
commit_id/restore_commit_id; thiếu ->400(commit_id is required). - Check commit nguồn thuộc section; không có ->
404(Commit not found). - Insert commit mới với
restored_from_commit_id = source.id. - 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):
- Load section; không có ->
404. ensureSectionState.assertLockAvailable(chỉ check không bị user khác lock).- status phải là
editing, không thì409(Section is not editable). head_commit_idphải có, không thì400(Section has no head commit to submit).- Load commit head; không có ->
404(Commit not found). - Insert
section_submissions(statuspending) copy snapshot từ commit head. - 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/approvePOST /submissions/:submissionId/reject
8.1. POST /submissions/:submissionId/reject
Luồng xử lý (transaction):
- Load submission; không có ->
404(Submission not found). - Submission phải
pending, không thì409(Submission is not pending). - Update submission:
status = rejected- set
reviewed_by/reviewed_at/review_note
- Update
section_states.status = rejected. - Không apply published tables.
Response 200: SectionSubmission (có snapshot).
8.2. POST /submissions/:submissionId/approve
Luồng xử lý (transaction):
- Load submission; không có ->
404. - Submission phải
pending, không thì409. - Verify snapshot hash:
- nếu
submission.snapshot_hashtồn tại và khácsha256(submission.snapshot_json)->409(Submission snapshot hash mismatch).
- nếu
- Parse snapshot JSON.
applySnapshotToPublished(snapshot, now)theo thứ tự:- apply
entities - apply
geometries - apply
link_scopes(replace links)
- apply
- Update submission:
status = approved- set
reviewed_by/reviewed_at/review_note
- 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 = conflictedvà setreview_note = <conflict message>nếu submission vẫn cònpending. - trả
409cho client.
- (best-effort) update
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 | replacereplaceđược normalize thànhupdatecho entity/geometry.
9.2. Validate Snapshot (400)
Backend validate ở /commits và trước khi approve:
snapshot.section.idvàsnapshot.section.titlebắt buộc.- Mỗi entity/geometry trong snapshot bắt buộc có
idvàoperationhợp lệ. - Entity:
namebắt buộc nếu không phảidelete|referenceslug(nếu có) không được trùng trong cùng snapshot (case-insensitive)
- Geometry:
- nếu
create|update|replacethì bắt buộc códraw_geometryhoặcgeometry - nếu có cả
time_start/time_endthìtime_start <= time_end bboxnếu gửi thì phải hợp lệ; nếu không gửi, approve sẽ tự tính bbox từ geometrybindingkhông được chứa chínhgeometry.id
- nếu
- Link scope:
geometry_idbắt buộc,entity_idsphải là arrayentity_idssau normalize không được rỗng
9.3. Apply Snapshot (Approve)
Entities
create: INSERTentities(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ằngbase_updated_atvà/hoặcbase_hash(nếu snapshot item gửi).delete: setentities.is_deleted = 1, xóa mọi link trongentity_geometries.reference: bỏ qua.
Geometries
create: INSERTgeometries:draw_geometryđược stringify JSONbbox: nếu snapshot cóbboxhợp lệ thì dùng, nếu không thì compute bằnggetBBox(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_geometriestheogeometry_id - remove mọi reference tới geometry đó trong
bindingcủa các geometry khác.
- set
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_idskhô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
INSERTlại toàn bộ link theo thứ tựentity_idsgử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_atmismatch hoặcbase_hashmismatch.- Link scope geometry không tồn tại/đã deleted.
base_links_hashmismatch.
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
- List section:
GET /sections. - Open editor (và lock):
GET /sections/:sectionId/editor?user_id=<actor>. - User edit local -> tạo snapshot.
- Commit:
POST /sections/:sectionId/commits(khuyến nghị gửiexpected_version+expected_head_commit_idtừ state). - (Optional) Restore:
POST /sections/:sectionId/restore. - Submit:
POST /sections/:sectionId/submit.
11.2. Reviewer Flow
- List sections:
GET /sections. - Với từng section:
GET /sections/:sectionId/submissions?include_snapshot=1. - Approve/reject:
POST /submissions/:submissionId/approvePOST /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/editorluô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.