14 KiB
Pending endpoints — frontend wishlist (ARCHIVED — все 5 shipped)
Историческая записка. Этот документ собирал brief из 5 endpoint'ов которые блокировали admin-UI fичи в sprint UI polish (MR !74→!93, май 2026). Все 5 теперь shipped — раздел остаётся для контекста и ссылок из коммитов, но для актуальной API-документации смотри: read-endpoints.md, schema-workflow.md, events.md.
Status (2026-05-12 — final)
| # | Endpoint | Status |
|---|---|---|
| 1 | GET /dictionaries/{name}/changelog |
✅ shipped (MR !96 + расширен MR !116 — workflow events) |
| 2 | GET /dictionaries/{name}/snapshots |
✅ shipped (MR !97) |
| 3 | Workflow API (8 endpoints) | ✅ shipped (MR !113 в v2.2.0) → schema-workflow.md |
| 4 | PATCH /dictionaries/{name}/schema |
✅ shipped (MR !98) |
| 5 | GET /dictionaries/graph/outgoing |
✅ shipped (MR !95) |
5/5 endpoints landed. Brief полностью закрыт.
- Changelog в MR !116 расширен event types для draft workflow
(
SCHEMA_DRAFT_CREATED,REVIEW_REQUESTED,DECIDED,PUBLISHED,WITHDRAWN). Frontend History tab теперь показывает полный timeline с промежуточными draft стадиями. Entries имеют полеkindдля рендера icon/color по типу события. - Workflow API (#3) развернулось в 8 endpoints (4 core + 4 read-only):
create draft / submit review / decide / publish + list / active / get / pool
queue. Liquibase 0022 добавил таблицу
dictionary_schema_draftsсо state machine guards (EXCLUDE on active, no-self-approve check). Полная спецификация: schema-workflow.md.
1. Schema versions timeline — GET /api/v1/dictionaries/{name}/changelog
Purpose: History tab в editor (/dictionaries/$name?tab=history) и
Time-travel modal — список всех schema versions с change summaries.
Frontend usage:
HistoryTabContent(routes/dictionaries.$name.tsx:HistoryTabContent) показывает HEAD row + N исторических versions сv{N} +X ~Y −Zchange summary, who/when, diff link.TimeTravelModalиспользует timestamps versions для slider marks.
Route: GET /api/v1/dictionaries/{name}/changelog
Query params:
| param | type | default | description |
|---|---|---|---|
limit |
int | 50 | max entries (cap 200) |
cursor |
string | — | opaque pagination token (next-page link) |
from |
ISO datetime | — | filter versions с publishedAt >= from |
to |
ISO datetime | — | filter versions с publishedAt <= to |
Response 200:
{
"dictionary": "satellites",
"currentVersion": "1.4.0",
"entries": [
{
"version": "1.4.0",
"publishedAt": "2026-04-12T10:23:00Z",
"publishedBy": { "sub": "u-42", "name": "zimin.an" },
"headers": "x-references массив + поле altitude",
"diff": {
"added": ["properties.altitude"],
"removed": [],
"changed": ["properties.operator (enum +1 value)"]
},
"recordsAffected": 0,
"isCurrent": true
},
{
"version": "1.3.2",
"publishedAt": "2026-03-30T08:11:00Z",
"publishedBy": { "sub": "u-17", "name": "petrov.iv" },
"headers": "fix description перевод EN",
"diff": { "added": [], "removed": [], "changed": ["description.en"] },
"recordsAffected": 0,
"isCurrent": false
}
],
"nextCursor": "eyJ2IjoiMS4zLjEiLCJ0IjoiMjAyNi0wMy0xNVQwMDowMDowMFoifQ=="
}
Errors: 404 dict not found, 401 unauth.
Notes:
diff— best-effort summary, не полный JSON-patch. Если backend считает дорого — допустим только counts:{ "added": 3, "removed": 0, "changed": 2 }.headers≈ commit message от автора schema edit. Optional; покажем "—" если empty.
Complexity: medium. Нужна таблица dictionary_schema_history (если
ещё нет — версия + JSON snapshot + author + timestamp).
1.1 Full diff payload — GET /api/v1/dictionaries/{name}/changelog/{entryId}/diff
✅ Shipped v2.4.0+ (MR !120).
Возвращает полные before / after schemas для конкретной changelog
entry — фронт делает side-by-side render в History tab / TimeTravel
preview. entryId приходит из ChangelogEntry.id field.
Response:
{
"id": 4711,
"dictionaryName": "satellite",
"eventType": "DEFINITION_UPDATED",
"kind": "definition_update",
"occurredAt": "2026-05-12T10:00:00Z",
"publishedBy": { "sub": "u-42", "name": "zimin.an" },
"before": { "type": "object", "properties": { ... } },
"after": { "type": "object", "properties": { ... } },
"summary": {
"added": ["properties.altitude"],
"removed": [],
"changed": ["properties.operator"]
}
}
Errors:
- 404
changelog_entry_not_found— id не существует, принадлежит другому dict'у, либо не schema-timeline event (record-level/unrelated). Existence не утекает через ID enumeration (одинаковая ошибка для всех 404 кейсов).
2. Records snapshots — GET /api/v1/dictionaries/{name}/snapshots
Purpose: Time-travel modal slider marks — точки во времени где дествительно произошёл change в records (не каждая секунда). UX: slider snaps к "интересным" моментам.
Frontend usage:
TimeTravelModal(components/timetravel/TimeTravelModal.tsx) рендерит marks на slider — каждая mark = snapshot point. Сейчас используемrecordTimestampsderived изvalidFrom— но это не учитывает closes.
Route: GET /api/v1/dictionaries/{name}/snapshots
Query params:
| param | type | default | description |
|---|---|---|---|
from |
ISO datetime | now − 30d | start of window |
to |
ISO datetime | now | end of window |
granularity |
string | day |
hour / day / week — bucket size |
Response 200:
{
"dictionary": "satellites",
"from": "2026-04-01T00:00:00Z",
"to": "2026-05-11T00:00:00Z",
"snapshots": [
{
"at": "2026-04-01T00:00:00Z",
"totalRecords": 124,
"changedSinceLast": 0,
"label": "1 апр"
},
{
"at": "2026-04-15T14:00:00Z",
"totalRecords": 127,
"changedSinceLast": 3,
"label": "15 апр"
},
{
"at": "2026-05-11T00:00:00Z",
"totalRecords": 130,
"changedSinceLast": 5,
"label": "сейчас",
"isCurrent": true
}
]
}
Notes:
- Backend free выбрать какие точки маркировать — main критерий ≥1 change с прошлой snapshot. Пустые часы/дни skip.
label— backend-formatted RU label для UI (избегаем locale logic на front).
Complexity: medium. Можно делать SELECT DISTINCT date_trunc('day', valid_from)
- count(*) over window. Кеш ~5min на горячих dict'ах.
3. Workflow state machine — workflow API
Purpose: Workflow buttons в editor toolbar (Request review,
Approve, Publish, Create draft). Сейчас показываются placeholder /
hidden когда approvalRequired=true потому что нет actions.
Frontend usage:
WorkflowBanner(components/editor/WorkflowBanner.tsx) показывает "live" / "pending review" / "draft" badge.- Action buttons прячутся пока нет API. После shipping — wire'им кнопки в banner / editor toolbar.
Routes (4 endpoints, RESTful):
3.1 POST /api/v1/dictionaries/{name}/drafts
Create draft от текущего HEAD. Returns draft id.
{ "fromVersion": "1.4.0", "reason": "обновить altitude диапазон" }
Response:
{ "draftId": "d-42a7", "branchedFrom": "1.4.0", "status": "draft" }
3.2 POST /api/v1/dictionaries/{name}/drafts/{draftId}/review
Submit draft for review.
{ "reviewers": ["u-17", "u-92"], "note": "проверьте altitude rounding" }
Response: 202 Accepted, { "status": "review_pending" }.
3.3 POST /api/v1/dictionaries/{name}/drafts/{draftId}/decision
Reviewer's decision.
{ "decision": "approve", "comment": "OK" }
decision: approve | request_changes | reject. Response 200 с new status.
3.4 POST /api/v1/dictionaries/{name}/drafts/{draftId}/publish
Publish approved draft → becomes new HEAD. Optimistic-lock на текущую version.
{ "expectedHeadVersion": "1.4.0", "publishNote": "v1.5.0 — altitude диапазон" }
Response 200:
{ "newVersion": "1.5.0", "publishedAt": "2026-05-11T22:00:00Z" }
409 если кто-то другой опубликовал свой draft первым (concurrent publish race).
Notes:
- State machine:
draft→review_pending→ (approved|changes_requested|rejected) → (publish | discard). - Все 4 endpoint'а должны вернуть updated draft state — frontend invalidate'ит query keys по dictionary name.
- RBAC: только approvers могут вызвать 3.3 с decision=approve.
Complexity: high. Нужны таблицы drafts, draft_reviews, state transition
guards. Webhook event hooks (frontend ожидает dict.draft.* events).
4. PATCH schema fields — PATCH /api/v1/dictionaries/{name}/schema
Purpose: Inline edit single field в Fields tab без открытия full DictionaryEditorDialog. UX: clickable field row → inline form → save → HEAD bumps minor version.
Frontend usage:
FieldsTabContent(routes/dictionaries.$name.tsx:FieldsTabContent) сейчас read-only. После endpoint — кнопка edit на каждой row.
Route: PATCH /api/v1/dictionaries/{name}/schema
Body (RFC 6902 JSON-patch subset или structured ops — backend выбирает):
Option A — JSON Patch:
{
"expectedHeadVersion": "1.4.0",
"ops": [
{
"op": "replace",
"path": "/properties/altitude/description",
"value": { "ru": "Высота орбиты в км", "en": "Orbit altitude in km" }
},
{ "op": "add", "path": "/properties/altitude/minimum", "value": 0 }
]
}
Option B — structured (более ergonomic для UI):
{
"expectedHeadVersion": "1.4.0",
"field": "altitude",
"changes": {
"description": { "ru": "...", "en": "..." },
"minimum": 0
}
}
Constraints: допустимые ops — non-breaking changes только (description,
title, minimum/maximum bounds expansion, enum value добавление). Breaking
ops (rename, type change, remove) → 422 с code: "breaking_change_requires_draft".
Response 200:
{
"newVersion": "1.4.1",
"schemaJson": { /* full updated schema */ }
}
Errors:
| code | status | meaning |
|---|---|---|
breaking_change_requires_draft |
422 | op требует workflow draft, не inline patch |
version_mismatch |
409 | someone else patched schema first (refetch) |
validation_failed |
422 | op breaks existing record validation |
Complexity: medium-high. Нужен validator который проверяет что patch не breaks существующие записи (semver-minor только).
5. Batch outgoing FK counts — GET /api/v1/dictionaries/graph/outgoing
Purpose: Catalog list (/dictionaries) — колонка → показывает
сколько FK rel'ов идёт ОТ каждого dict'а. Сейчас на front считаем
loop'ом per-row через extractOutgoingFkCount(schemaJson) — работает,
но требует все schemaJson на клиенте.
Frontend usage:
routes/dictionaries.index.tsxcatalog table —→column показывает outgoing FK count. Если backend вернёт батчом — frontend сделает 1 request вместо включения schemaJson в каждую row useDictionaries.
Route: GET /api/v1/dictionaries/graph/outgoing?dicts=satellites,operators,...
Query params:
| param | type | default | description |
|---|---|---|---|
dicts |
comma-separated string | all visible | optional filter |
scope |
comma-separated string | all | PUBLIC,INTERNAL etc |
Response 200:
{
"outgoing": {
"satellites": { "fkCount": 3, "targets": ["operators", "launch_sites", "missions"] },
"operators": { "fkCount": 0, "targets": [] },
"missions": { "fkCount": 2, "targets": ["operators", "rockets"] }
}
}
Notes:
targetsопционально — если дорого, можно вернуть толькоfkCount.- Скорость: вся таблица обычно ≤200 dict'ов — простой SELECT с
jsonb_array_length(schema_json->'x-references')илиLATERALenumeration. Кеш 60s.
Complexity: low. Это derived data из существующих schemaJson — просто batch SELECT + aggregate.
Frontend integration timeline
| Endpoint | Component | Estimate after backend ship |
|---|---|---|
| 1. Changelog | HistoryTabContent, TimeTravelModal |
~2h (Orval hook + render entries) |
| 2. Snapshots | TimeTravelModal slider marks |
~1h (replace mock derivation) |
| 3. Workflow API | WorkflowBanner + new buttons in toolbar |
~6h (4 mutations + state UI) |
| 4. PATCH schema | FieldsTabContent inline edit |
~4h (form + diff preview) |
| 5. Batch graph | dictionaries.index → column |
~30min (single query swap) |
Total frontend work post-backend: ~14h (1.5 sprint days). Per endpoint frontend готов в течение часа после OpenAPI freeze.
Conventions reminder
- Все endpoints должны быть Authenticated (Keycloak JWT, см. auth.md).
- Errors следуют errors.md —
{ code, message, details? }. - Optimistic-lock 409 везде где есть concurrent edits.
- Webhook events для async state changes — см. webhooks.md.
- Idempotency-Key header на mutating endpoints (Time-travel race safety per existing API guidelines).