411 lines
14 KiB
Markdown
411 lines
14 KiB
Markdown
# 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](read-endpoints.md), [schema-workflow.md](schema-workflow.md),
|
||
> [events.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](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](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 −Z`
|
||
change 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:**
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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. Сейчас используем
|
||
`recordTimestamps` derived из `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:**
|
||
|
||
```json
|
||
{
|
||
"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.
|
||
|
||
```json
|
||
{ "fromVersion": "1.4.0", "reason": "обновить altitude диапазон" }
|
||
```
|
||
|
||
Response:
|
||
```json
|
||
{ "draftId": "d-42a7", "branchedFrom": "1.4.0", "status": "draft" }
|
||
```
|
||
|
||
### 3.2 `POST /api/v1/dictionaries/{name}/drafts/{draftId}/review`
|
||
|
||
Submit draft for review.
|
||
|
||
```json
|
||
{ "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.
|
||
|
||
```json
|
||
{ "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.
|
||
|
||
```json
|
||
{ "expectedHeadVersion": "1.4.0", "publishNote": "v1.5.0 — altitude диапазон" }
|
||
```
|
||
|
||
Response 200:
|
||
```json
|
||
{ "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:
|
||
```json
|
||
{
|
||
"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):
|
||
```json
|
||
{
|
||
"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:**
|
||
|
||
```json
|
||
{
|
||
"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.tsx` catalog 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:**
|
||
|
||
```json
|
||
{
|
||
"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')` или `LATERAL`
|
||
enumeration. Кеш 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](auth.md)).
|
||
- Errors следуют [errors.md](errors.md) — `{ code, message, details? }`.
|
||
- Optimistic-lock 409 везде где есть concurrent edits.
|
||
- Webhook events для async state changes — см. [webhooks.md](webhooks.md).
|
||
- Idempotency-Key header на mutating endpoints (Time-travel race
|
||
safety per existing API guidelines).
|