docs(integrator): v2.x.x update — workflow API + full changelog + schema extensions

This commit is contained in:
Александр Зимин
2026-05-12 00:26:37 +00:00
parent bc3373ccd8
commit d790a77313
7 changed files with 552 additions and 17 deletions
+31
View File
@@ -97,6 +97,37 @@ artifact в `~/.gstack/projects/claude/`) на каждую закрытую з
`cascadeSource` = `{dictionary, businessKey}` источника. Consumer может
trace cascade chain.
### Definition events
Изменения **самой schema** dictionary (не records) emit'ятся отдельными
event types:
| `eventType` | Когда |
|---|---|
| `DefinitionCreated` | Новый dictionary зарегистрирован |
| `DefinitionUpdated` | Inline PATCH schema (см. `/dictionaries/{name}/schema`) |
Payload содержит `schemaJson`, `schemaVersion`, `previousSchema` (для
updated). Consumer'ы которые кэшируют schemas инвалидируют по этим
событиям.
### Schema workflow events (v2.2.0+)
Workflow draft lifecycle emit'ит дополнительные events. Все содержат
`draftId`, `dictionaryName`, `status`, `branchedFromVersion`, `makerId`
плюс event-specific extras:
| `eventType` | Trigger | Extras |
|---|---|---|
| `SchemaDraftCreated` | maker создаёт draft | `fromVersion` |
| `SchemaDraftReviewRequested` | submit на review | `reviewers[]`, `note` |
| `SchemaDraftDecided` | reviewer вынес verdict | `decision` (approve/request_changes/reject), `reviewerId`, `comment` |
| `SchemaDraftPublished` | publisher выпустил draft → live | `newVersion`, `publishedAt`, `publishNote` |
| `SchemaDraftWithdrawn` | maker отозвал draft | (без extras) |
Webhook router маппит эти типы в `dict.draft.*` category (см.
[webhooks.md](webhooks.md)). Полная state machine: [schema-workflow.md](schema-workflow.md).
## Partition key
Сообщения партиционируются по `dictionary + businessKey` (одна запись —
+4
View File
@@ -26,6 +26,10 @@
- **Webhooks** — push-уведомления HTTP к зарегистрированным URLs (для
consumers без Kafka access).
- **Bulk operations** — multi-select close + CSV export.
- **Schema workflow** — maker-checker approval для изменения schemas
(drafts + review + publish). См. [Schema workflow API](schema-workflow.md).
- **Full changelog timeline** — `/dictionaries/{name}/changelog` объединяет
definition CRUD + draft workflow events с `kind` field для UI rendering.
## Что Ordinis НЕ делает
+22 -16
View File
@@ -1,28 +1,34 @@
# Pending endpoints — frontend wishlist
# Pending endpoints — frontend wishlist (ARCHIVED — все 5 shipped)
Эти 5 endpoint'ов нужны admin-UI для завершения UI-фич, заплейсхолженных в
sprint UI polish (MR !74→!93, mai-2026). Каждый блокирует конкретный
component на frontend — сегодня там показывается placeholder ("backend
changelog pending" notice / disabled button / `0` count).
> **Историческая записка.** Этот документ собирал 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).
Frontend готов подключить через Orval — нужны только OpenAPI спеки.
Приоритет в порядке user-impact: 1 (highest) → 5 (nice-to-have).
## Status (2026-05-11)
## Status (2026-05-12 — final)
| # | Endpoint | Status |
|---|---|---|
| 1 | `GET /dictionaries/{name}/changelog` | ✅ shipped (MR !96) |
| 1 | `GET /dictionaries/{name}/changelog` | ✅ shipped (MR !96 + расширен MR !116 — workflow events) |
| 2 | `GET /dictionaries/{name}/snapshots` | ✅ shipped (MR !97) |
| 3 | Workflow API (4 endpoints) | ⏸ deferred — separate design + sprint |
| 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) |
**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.
**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).
---
+420
View File
@@ -0,0 +1,420 @@
# Schema workflow API (drafts / review / publish)
Maker-checker approval workflow для schema-level изменений.
Параллельная архитектура к record-level `DraftController` (см.
[bitemporal.md](bitemporal.md)), но drafts содержат **полное proposed
JSON Schema** dictionary'а, а не отдельные записи.
Доступно с **v2.2.0**.
## Зачем
Schema изменения (добавить поле, поменять enum, поменять `x-references`)
часто требуют согласования: maker готовит изменение, reviewer одобряет,
publisher выпускает новую semver. Workflow API даёт state machine для
этого процесса с audit trail в `audit_log` + Kafka outbox events
(`SchemaDraft*`) + lock на concurrent drafts.
Альтернатива workflow: inline `PATCH /api/v1/dictionaries/{name}/schema`
для **non-breaking** изменений (см. [read-endpoints.md](read-endpoints.md)).
Если PATCH возвращает 422 `breaking_change_requires_draft` — переходи на
workflow.
## State machine
```
┌──────────────────────────────────┐
▼ │
┌──────────────┐ │
create ──▶│ DRAFT │──── withdraw ────┐ │
└──────┬───────┘ │ │
│ submit_review │ │
▼ │ │
┌──────────────────┐ │ │
│ REVIEW_PENDING │── withdraw ────┤ │
└──────┬───┬───┬───┘ │ │
approve│ │reject │ │
│ └─request_changes─┐ │ │
▼ ▼ ▼ │
┌──────────┐ ┌─────────────────┐ │
│ APPROVED │ │CHANGES_REQUESTED│────┘
└─────┬────┘ └───────┬─────────┘ re-submit
publish │ │
▼ ▼ withdraw
┌─────────────┐ ┌───────────┐
│ PUBLISHED │ │ WITHDRAWN │
│ (terminal) │ │ (terminal)│
└─────────────┘ └───────────┘
┌───────────┐
│ REJECTED │ (terminal — от reject)
└───────────┘
```
**Терминальные** статусы: `PUBLISHED`, `REJECTED`, `WITHDRAWN` — изменению
дальше не подлежат, но остаются в БД для audit trail.
**Invariants** (enforced на уровне Liquibase 0022 + service):
- Maximum один active draft per dictionary
(`schema_drafts_one_active_per_dict` EXCLUDE constraint покрывает
`draft|review_pending|changes_requested|approved`).
- Self-approve forbidden (`schema_drafts_no_self_approve`: `maker_id !=
decision_user_id`).
- Optimistic version lock на create + publish — `fromVersion` /
`expectedHeadVersion` сверяется с текущим `definition.schemaVersion`,
если разошлось → 409 `version_mismatch`.
## Endpoints
### Создание / переходы
#### `POST /api/v1/dictionaries/{name}/drafts`
Создаёт draft от текущего HEAD schema. Status = `DRAFT`.
Request:
```json
{
"fromVersion": "1.4.0",
"reason": "добавить optional поле altitude",
"proposedSchema": { "type": "object", "properties": { ... } }
}
```
Response 201 (см. [Полный response shape](#полный-response-shape) ниже):
```json
{
"draftId": "8f3...",
"dictionaryName": "satellite",
"status": "draft",
"branchedFromVersion": "1.4.0",
...
}
```
Errors:
- 409 `version_mismatch``fromVersion` не = current HEAD.
- 409 `active_draft_exists` — другой draft уже active на этом dict'е
(терминальные не блокируют).
---
#### `POST /api/v1/dictionaries/{name}/drafts/{draftId}/review`
Maker submit'ит draft → status `REVIEW_PENDING`.
Работает из `DRAFT` (initial submit) и `CHANGES_REQUESTED` (re-submit
после доработки).
Request:
```json
{
"reviewers": ["u-17", "u-92"], // optional — NULL = open pool (anyone with approve role)
"note": "проверьте границы altitude"
}
```
Response 202:
```json
{ "status": "review_pending", ... }
```
Errors:
- 403 `maker_only` — actor != maker.
- 409 `invalid_transition` — draft не в DRAFT или CHANGES_REQUESTED.
---
#### `POST /api/v1/dictionaries/{name}/drafts/{draftId}/decision`
Reviewer's decision.
Request:
```json
{
"decision": "APPROVE", // или "REQUEST_CHANGES" или "REJECT"
"comment": "OK по полю, но опечатка в описании"
}
```
Verdict mapping:
- `APPROVE` → status `APPROVED` (готов к publish)
- `REQUEST_CHANGES` → status `CHANGES_REQUESTED` (maker должен доработать)
- `REJECT` → status `REJECTED` (terminal)
Response 200 — `SchemaDraftResponse` с updated status.
Errors:
- 403 `self_approve_forbidden``actor == maker`.
- 409 `invalid_transition` — draft не в `REVIEW_PENDING`.
---
#### `POST /api/v1/dictionaries/{name}/drafts/{draftId}/publish`
Publish approved draft → live HEAD. Bump schema version (minor default,
major на breaking changes — type change OR required removal в top-level
properties).
Request:
```json
{
"expectedHeadVersion": "1.4.0",
"publishNote": "v1.5.0 — добавил altitude"
}
```
Response 200:
```json
{
"draft": { "status": "published", ... },
"newVersion": "1.5.0",
"publishedAt": "2026-05-12T22:00:00Z"
}
```
Side effects:
- `dictionary_definitions.schema_json` и `.schema_version` обновляются
в одной SERIALIZABLE транзакции.
- Audit log: `SCHEMA_DRAFT_PUBLISHED` (`DRAFT_PUBLISH` action).
- Outbox event: `SchemaDraftPublished`.
- WAL/Kafka: subscribers получают definition update в обычном порядке.
Errors:
- 409 `invalid_transition` — draft не в `APPROVED`.
- 409 `version_mismatch``expectedHeadVersion` stale (кто-то другой
опубликовал свой draft первым). Refetch + retry.
---
#### `DELETE /api/v1/dictionaries/{name}/drafts/{draftId}`
Maker отзывает свой draft. Status → `WITHDRAWN` (terminal).
Работает из любого non-terminal статуса.
Errors:
- 403 `maker_only` — actor != maker.
- 409 `invalid_transition` — draft уже в terminal статусе.
### Read-only
#### `GET /api/v1/dictionaries/{name}/drafts`
List всех drafts на dict (newest first). Включает terminal — для audit
trail UI.
#### `GET /api/v1/dictionaries/{name}/drafts/active`
Convenience: возвращает текущий **active** draft (любой non-terminal статус)
для UI workflow banner. **404 `no_active_draft`** если нет — это нормальный
сигнал, фронт показывает «no draft» состояние.
#### `GET /api/v1/dictionaries/{name}/drafts/{draftId}`
Single draft по id (drilling в reviewer queue).
#### `GET /api/v1/admin/schema-reviews/pending`
Global pool queue для approvers — все `review_pending` drafts across all
dicts, sorted oldest-first (fairness). Paginated:
```
?page=0&size=50
```
Response:
```json
{
"items": [ { "draftId": "...", "dictionaryName": "satellite", ... } ],
"page": 0,
"size": 50,
"totalElements": 7,
"totalPages": 1
}
```
## Полный response shape
`SchemaDraftResponse`:
```json
{
"draftId": "8f3a...",
"dictionaryId": "5fe1...",
"dictionaryName": "satellite",
"status": "review_pending", // см. State machine
"branchedFromVersion": "1.4.0",
"proposedSchema": { ... }, // full JSON Schema
"reason": "добавить altitude", // optional
"makerId": "u-42",
"createdAt": "2026-05-12T20:00:00Z",
"submittedAt": "2026-05-12T20:05:00Z", // NULL до submitForReview
"reviewers": ["u-17", "u-92"], // NULL = pool
"reviewNote": "проверьте altitude", // optional
"decisionUserId": null, // set после decide
"decisionAt": null,
"decisionComment": null,
"publishedAt": null, // set после publish
"publishedVersion": null,
"publishNote": null,
"version": 1 // optimistic lock counter
}
```
## Outbox events
Каждый transition emit'ит event в Kafka outbox topic `definition.draft`
(routed через webhook subscribers как `dict.draft.*` category).
| Event type | Trigger | Payload extras |
|---|---|---|
| `SchemaDraftCreated` | POST /drafts | `fromVersion` |
| `SchemaDraftReviewRequested` | POST /review | `reviewers[]`, `note` |
| `SchemaDraftDecided` | POST /decision | `decision`, `reviewerId`, `comment` |
| `SchemaDraftPublished` | POST /publish | `newVersion`, `publishedAt`, `publishNote` |
| `SchemaDraftWithdrawn` | DELETE | (без extras) |
Envelope:
```json
{
"eventId": "...",
"occurredAt": "2026-05-12T20:00:00Z",
"schemaVersion": "1.0",
"bundle": "cuod",
"dictionaryName": "satellite",
"scope": "PUBLIC",
"payload": { "draftId": "...", "dictionaryName": "...", "status": "...", ... }
}
```
## Audit log
Параллельно outbox'у каждое transition пишется в `audit_log` через
`AuditLogger.schemaDraftEvent`:
| `event_type` | `action` | `payload_before` | `payload_after` |
|---|---|---|---|
| `SCHEMA_DRAFT_CREATED` | `DRAFT_CREATE` | live schema | proposed schema |
| `SCHEMA_DRAFT_REVIEW_REQUESTED` | `DRAFT_REVIEW` | live schema | proposed schema |
| `SCHEMA_DRAFT_DECIDED` | `DRAFT_APPROVE` / `DRAFT_REJECT` / `DRAFT_CHANGES_REQUESTED` | live | proposed |
| `SCHEMA_DRAFT_PUBLISHED` | `DRAFT_PUBLISH` | previous live | new live (= proposed) |
| `SCHEMA_DRAFT_WITHDRAWN` | `DRAFT_WITHDRAW` | live | proposed |
`payload_before` / `payload_after` доступны через `GET /admin/audit` UI
для diff render'а в History timeline.
Также эти event_types включены в `GET /api/v1/dictionaries/{name}/changelog`
— см. [Full timeline в changelog](#full-timeline-в-changelog) ниже.
## Full timeline в changelog
`GET /api/v1/dictionaries/{name}/changelog` после MR !116 возвращает unified
timeline definition-level + workflow draft events. Каждая entry имеет:
```json
{
"version": "1.5.0", // только для versioning events
"publishedAt": "2026-05-12T20:23:00Z",
"publishedBy": { "sub": "u-42", "name": "..." },
"headers": null, // commit msg — не captured пока
"diff": { "added": [...], "removed": [...], "changed": [...] },
"recordsAffected": 0,
"isCurrent": true,
"kind": "draft_publish" // ← NEW field в MR !116
}
```
Поле `kind`:
| Value | Source event_type |
|---|---|
| `definition_create` | DEFINITION_CREATED |
| `definition_update` | DEFINITION_UPDATED (inline PATCH path) |
| `draft_create` | SCHEMA_DRAFT_CREATED |
| `draft_review` | SCHEMA_DRAFT_REVIEW_REQUESTED |
| `draft_approve` | SCHEMA_DRAFT_DECIDED + action=DRAFT_APPROVE |
| `draft_reject` | SCHEMA_DRAFT_DECIDED + action=DRAFT_REJECT |
| `draft_changes_requested` | SCHEMA_DRAFT_DECIDED + action=DRAFT_CHANGES_REQUESTED |
| `draft_publish` | SCHEMA_DRAFT_PUBLISHED ← versioning event |
| `draft_withdraw` | SCHEMA_DRAFT_WITHDRAWN |
| `other` | fallback (unknown event_type) |
**Versioning events** (`definition_create`, `definition_update`,
`draft_publish`) получают `version` label из `dictionary_definitions.schema_version`
для current HEAD, NULL для исторических entries (semver backfill TBD).
**Promotional events** (`draft_create`, `draft_review`, `draft_approve`,
`draft_reject`, `draft_changes_requested`, `draft_withdraw`) возвращаются
с `version=null` — фронт рендерит как timeline tick между публикациями.
## Пример workflow
Полный flow «изменить enum в `mission.status`»:
```bash
# 0. Получить current HEAD
curl /api/v1/dictionaries/mission
# → schemaVersion: "1.4.0", schemaJson: { ... }
# 1. Создать draft с новым enum
curl -X POST /api/v1/dictionaries/mission/drafts -d '{
"fromVersion": "1.4.0",
"reason": "добавить status=deferred",
"proposedSchema": { ... }
}'
# → 201, draftId: "8f3a..."
# 2. Submit на review
curl -X POST /api/v1/dictionaries/mission/drafts/8f3a.../review -d '{
"reviewers": ["u-17"],
"note": "deferred — это inactive но не decommissioned"
}'
# → 202, status: "review_pending"
# 3. Reviewer u-17 (другой actor) одобряет
curl -X POST /api/v1/dictionaries/mission/drafts/8f3a.../decision -d '{
"decision": "APPROVE",
"comment": "OK"
}'
# → 200, status: "approved"
# 4. Publisher (любой actor != maker) publish'ит
curl -X POST /api/v1/dictionaries/mission/drafts/8f3a.../publish -d '{
"expectedHeadVersion": "1.4.0",
"publishNote": "v1.5.0 — status enum +deferred"
}'
# → 200, newVersion: "1.5.0"
```
После publish:
- `dictionary_definitions.schema_version` = "1.5.0"
- `dictionary_definitions.schema_json` = новый schema
- `audit_log` строки: SCHEMA_DRAFT_CREATED, REVIEW_REQUESTED, DECIDED
(APPROVE), PUBLISHED
- Kafka outbox: 4 события `SchemaDraft*`
- `changelog` endpoint показывает полную цепочку с `kind` field
- Webhook subscribers получают `dict.draft.*` notifications
## Error codes (full reference)
| HTTP | Code | Когда |
|---|---|---|
| 404 | `schema_draft_not_found` | id не существует / scope-hidden |
| 404 | `dictionary_not_found` | dict не существует / scope-hidden |
| 404 | `no_active_draft` | `/drafts/active` нет non-terminal draft'а |
| 409 | `active_draft_exists` | попытка create при существующем active |
| 409 | `version_mismatch` | optimistic lock на create или publish |
| 409 | `invalid_transition` | state machine violation |
| 403 | `self_approve_forbidden` | maker decision на own draft |
| 403 | `maker_only` | submit/withdraw от чужого actor'а |
| 422 | `breaking_change_requires_draft` | inline PATCH вернул это — переходи в workflow |
## RBAC (TBD)
В текущей реализации workflow доступен любому authenticated user'у через
network-level auth policy. Когда Keycloak roles claim прорастёт в JWT,
будут отдельные требования:
- `dict_schema_maker` — для create / submit / withdraw
- `dict_schema_reviewer` — для decision
- `dict_schema_publisher` — для publish
См. record-level `DraftController` для reference architecture pool model.
+16
View File
@@ -39,8 +39,24 @@ curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
| `scopes` | array | Фильтр по scope (subset из доступных consumer'у). |
| `dictionaries` | array | Фильтр по dictionary names. `null`/`[]` = все доступные. |
| `actions` | array | `created` / `updated` / `closed`. `null` = все. |
| `categories` | array | Event categories: `dict.record.*` / `dict.definition.*` / `dict.draft.*`. `null` = все. (v2.2.0+) |
| `active` | bool | Можно временно отключить без удаления. |
### Event categories (v2.2.0+)
С v2.2.0 webhook subscriber'ы могут фильтровать по семантическим категориям:
| Category | Маппит outbox events |
|---|---|
| `dict.record.*` | Record CRUD (default до v2.2.0): record.created / .updated / .closed |
| `dict.definition.*` | Schema-level: `DefinitionCreated`, `DefinitionUpdated` (inline PATCH) |
| `dict.draft.*` | Workflow drafts: `SchemaDraftCreated`, `…ReviewRequested`, `…Decided`, `…Published`, `…Withdrawn` |
Если `categories=null` подписчик получает все (legacy behaviour).
Frontend reviewer dashboards обычно подписываются на `dict.draft.*`
для notifications о новых drafts. Bundle consumers (Альтум, BI) —
на `dict.record.*`.
## SSRF guard
Webhook URLs валидируются перед отправкой:
+56
View File
@@ -138,7 +138,63 @@ deployment v2 — следи за анонсами от Ordinis team.
4. **Document FK в displayName** — `"description": "FK на satellite_type
через code"` помогает при ревью schemas.
## Связанные schema extensions
### `x-localized` — мультиязычные поля
```json
{ "type": "object", "x-localized": true }
```
См. [read-endpoints.md](read-endpoints.md) `?lang=` параметр.
### `x-section` — bucketing полей в admin-UI form
Hint для admin-UI: в какую визуальную секцию формы (`identity`,
`references`, `description`, `dates`, `geometry`, `physical`, `extra`,
либо custom string) сгруппировать поле.
```json
{
"type": "number",
"x-section": "orbital_geometry"
}
```
Если `x-section` не задан, admin-UI применяет эвристику:
- `x-references``references`
- `x-localized``description`
- `format=date|date-time``dates`
- key matches `/^(geo|geom|wkt|lon|lng|lat|altitude|coord)/``geometry`
- `required``identity`
- else → `extra`
Predefined IDs получают i18n labels (Связи / Описание / Даты / Геометрия /
Физические параметры / Идентификация / Дополнительно). Custom IDs
humanize'ятся: `orbital_geometry``Orbital Geometry`.
Только UI hint — backend не validate'ит и не reject'ит unknown values.
### `x-id-source` — авто-derive businessKey
```json
{ "x-id-source": "code" }
```
При CREATE businessKey берётся из `data.code` automatically (UI скрывает
поле). См. ReferenceValidator.
### `x-unique` — уникальность поля в dict'е
```json
{ "type": "string", "x-unique": true }
```
Backend enforce'ит при CREATE/UPDATE. Используется для `code` полей в
target dicts которые ссылаются по `x-references`.
## Дальше
- [Bundle cuod](bundle-cuod.md) — список текущих FK в проде
- [События](events.md) — учитывай cascade closure при инвалидации
- [Schema workflow](schema-workflow.md) — как менять schema через approval flow
+3 -1
View File
@@ -29,7 +29,9 @@ items:
href: integration/api/best-practices.md
- name: Bundle справочников cuod
href: integration/api/bundle-cuod.md
- name: Pending endpoints (admin-UI wishlist)
- name: Schema workflow (drafts / review / publish)
href: integration/api/schema-workflow.md
- name: Pending endpoints (архив, все shipped)
href: integration/api/pending-endpoints.md
- name: Прикладные сценарии