Files
mdm-ordinis/docs/integration/api/pending-endpoints.md
T
Zimin A.N. a4add2402a docs(api): pending-endpoints brief for backend team
5 endpoints нужны admin-UI для завершения UI-фич:
  1. GET /dictionaries/{name}/changelog — History tab + TimeTravel
  2. GET /dictionaries/{name}/snapshots — TimeTravel slider marks
  3. POST drafts/.../{review,decision,publish} — workflow state machine
  4. PATCH /dictionaries/{name}/schema — Fields tab inline edit
  5. GET /dictionaries/graph/outgoing — catalog → FK count column

Каждый endpoint содержит: purpose, route, query/body, response example,
errors, frontend integration estimate. Total frontend work after backend
ship — ~14h (1.5 sprint day).
2026-05-11 22:47:24 +03:00

12 KiB
Raw Blame History

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).


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:

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

{
  "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: draftreview_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.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:

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