Files
mdm-ordinis/docs/integration/api/pending-endpoints.md
T
Zimin A.N. 6c8cc21204 docs(api): mark 4/5 pending endpoints shipped, workflow API deferred
Status table в pending-endpoints.md обновлён: endpoints #1, #2, #4, #5
landed в MR !95-!98 в session 2026-05-11. Workflow API (#3) deferred —
требует отдельной design discussion + sprint (state machine + RBAC pool
model parallel to record-level DraftController architecture).
2026-05-11 23:15:41 +03:00

371 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pending endpoints — frontend wishlist
Эти 5 endpoint'ов нужны admin-UI для завершения UI-фич, заплейсхолженных в
sprint UI polish (MR !74→!93, mai-2026). Каждый блокирует конкретный
component на frontend — сегодня там показывается placeholder ("backend
changelog pending" notice / disabled button / `0` count).
Frontend готов подключить через Orval — нужны только OpenAPI спеки.
Приоритет в порядке user-impact: 1 (highest) → 5 (nice-to-have).
## Status (2026-05-11)
| # | Endpoint | Status |
|---|---|---|
| 1 | `GET /dictionaries/{name}/changelog` | ✅ shipped (MR !96) |
| 2 | `GET /dictionaries/{name}/snapshots` | ✅ shipped (MR !97) |
| 3 | Workflow API (4 endpoints) | ⏸ deferred — separate design + sprint |
| 4 | `PATCH /dictionaries/{name}/schema` | ✅ shipped (MR !98) |
| 5 | `GET /dictionaries/graph/outgoing` | ✅ shipped (MR !95) |
**4/5 endpoints landed** в session 2026-05-11 backend sprint. Workflow API (#3)
требует отдельной design discussion — state machine + RBAC pool model
parallel record-level drafts (см. `DraftController.java` для record-level
draft architecture как reference). Frontend WorkflowBanner currently рендерит
placeholder banner — кнопки скрыты пока endpoints не shipped.
---
## 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).
---
## 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).