diff --git a/docs/integration/api-quickstart.md b/docs/integration/api-quickstart.md index 14df136..6850a16 100644 --- a/docs/integration/api-quickstart.md +++ b/docs/integration/api-quickstart.md @@ -579,8 +579,296 @@ DictionaryDetail detail = mapper.readValue(resp.body(), DictionaryDetail.class); ## 13. Дальше +- **Приложение A: Cheat Sheet** — одностраничник со всеми endpoints, ниже в этом документе - **OpenAPI:** `$ORDINIS/v3/api-docs` (staging) / Swagger UI выше +- **Postman collection:** `~/Desktop/Ordinis-Postman/` (27 рабочих запросов + 10-шаговый demo flow) - **Bundle spec (для интеграторов):** [`bundle-format-spec.md`](./bundle-format-spec.md) - **CHANGELOG:** [`docs/changelog.md`](../changelog.md) — что меняется по версиям - **Webhooks deep-dive:** в admin UI → `/webhooks` → Detail page показывает histogram доставок, DLQ, signature test + +--- + +# Приложение A · Cheat Sheet + +> Дублирует [`api-cheatsheet.md`](./api-cheatsheet.md) — одностраничный quick-reference со всеми endpoints, query-params, error codes и реальными примерами для copy-paste. Дублирование осознанное: интегратор открывает quickstart один раз, далее живёт в приложении. + + +Одностраничный референс. Полный гайд — `api-quickstart.md`. + +**Base prod:** `https://ordinis.nstart.cloud/api/v1` (public LE-cert) +**Base staging:** `https://ordinis.k8s.265.nstart.cloud/api/v1` (Swagger UI здесь) +**Version:** `v2.37.x` · **OpenAPI:** `/v3/api-docs` (только staging — на prod Swagger выключен) + +## Auth + +```bash +# client_credentials (для service-to-service интеграторов) +TOKEN=$(curl -s -X POST https://auth.nstart.space/realms/nstart/protocol/openid-connect/token \ + -d grant_type=client_credentials \ + -d client_id=$KC_CLIENT_ID -d client_secret=$KC_CLIENT_SECRET | jq -r .access_token) + +curl -H "Authorization: Bearer $TOKEN" $ORDINIS/api/v1/spacecraft/records?as_scope=PUBLIC,INTERNAL +``` + +Scopes: `PUBLIC` (anon ok) · `INTERNAL` (auth) · `RESTRICTED` (admin). + +## Read (no auth для PUBLIC) + +| Verb | Path | Что | +|---|---|---| +| `GET` | `/version` | Версия + commit + builtAt | +| `GET` | `/dictionaries` | Список всех справочников | +| `GET` | `/dictionaries/{name}` | Детали + JSON Schema | +| `GET` | `/dictionaries/templates` | 6 шаблонов схем | +| `GET` | `/dictionaries/templates/{id}` | Один шаблон со schemaJson | +| `GET` | `/dictionaries/graph/outgoing` | FK-граф между справочниками | +| `GET` | `/dictionaries/{name}/dependents` | Кто ссылается на этот dict | +| `GET` | `/dictionaries/{name}/changelog` | Список изменений схемы | +| `GET` | `/dictionaries/{name}/changelog/{id}/diff` | Diff между версиями | +| `GET` | `/dictionaries/{name}/snapshots?from=&to=&granularity=` | Bucket'ы изменений для TimeTravel slider (granularity: hour/day/week) | +| `GET` | `/{name}/records?as_scope=PUBLIC` | Список записей (⚠️ **БЕЗ** `/dictionaries/` префикса) | +| `GET` | `/{name}/records/{businessKey}?as_scope=PUBLIC` | Одна запись | +| `GET` | `/dictionaries/{name}/records/{businessKey}/history?as_scope=PUBLIC` | История версий записи (Envers) — **с** `/dictionaries/` | +| `GET` | `/dictionaries/{name}/records/{businessKey}/cascade-preview?as_scope=PUBLIC` | Preview каскада — **с** `/dictionaries/` | +| `GET` | `/{name}/records/scheduled-summary?as_scope=PUBLIC` | Будущие версии (count + nearestValidFrom) | +| `GET` | `/search?q=` | Поиск по справочникам (records не индексируются) | +| `GET` | `/ai/info` | `{enabled: bool}` AI feature flag | + +> ⚠️ **Корень частой путаницы**: consumer endpoints (`/records/...`) идут БЕЗ `/dictionaries/` префикса. Admin-style audit endpoints (history, cascade-preview, changelog, drafts, snapshots) — С `/dictionaries/`. См. [api-quickstart.md § Topology](api-quickstart.md). + +### Query params (records) + +``` +?as_scope=PUBLIC,INTERNAL (required) CSV scope filter +?at=2026-03-15T00:00:00Z bitemporal: запись на конкретный момент (validFrom <= at < validTo) +Accept-Language: en-US (header) локаль — единственный рабочий путь + ⚠️ ?locale= query param backend сейчас игнорирует + ⚠️ Работает ТОЛЬКО на records endpoints. + Schema endpoint (`/dictionaries/{name}`) всегда + возвращает schemaJson на defaultLocale. +?bbox=lon1,lat1,lon2,lat2 geo: bounding box (для geo-dicts) +?polygon= geo: polygon-фильтр +``` + +**Что НЕ работает (на read-api endpoint):** +- `?limit=`, `?offset=` — pagination отсутствует, возвращается весь массив +- `?status=ACTIVE&country=RU` — нет ad-hoc filtering по schema полям; фильтруй на клиенте + +## Write (требует auth) + +⚠️ Все write-endpoints — на **writer** сервисе, с `/dictionaries/` префиксом. + +| Verb | Path | Что | +|---|---|---| +| `POST` | `/dictionaries/{name}/records` | Создать запись (если approval не нужен) | +| `PUT` | `/dictionaries/{name}/records/{businessKey}` | Обновить | +| `DELETE` | `/dictionaries/{name}/records/{businessKey}` | Soft-delete (tombstone) | +| `POST` | `/dictionaries/{name}/records/bulk-close` | Массовое закрытие | +| `POST` | `/dictionaries/{name}/records/{businessKey}/cascade-close` | Закрыть с зависимыми | +| `POST` | `/dictionaries/{name}/records/export-selected.csv` | Bulk export (выборка по ids в body) | +| `PATCH` | `/dictionaries/{name}/schema` | Изменить схему inline (non-breaking) | +| `PUT` | `/dictionaries/{name}` | Полностью пересоздать (breaking) | + +## Workflow (drafts + approvals) + +| Verb | Path | Что | +|---|---|---| +| `POST` | `/dictionaries/{name}/records/drafts` | Создать record draft | +| `POST` | `/dictionaries/{name}/drafts` | Создать schema draft | +| `GET` | `/dictionaries/{name}/drafts` | Список schema drafts по dict | +| `GET` | `/dictionaries/{name}/drafts/active` | Активный draft (200+null если нет) | +| `GET` | `/dictionaries/{name}/drafts/{id}` | Один draft | +| `DELETE` | `/dictionaries/{name}/drafts/{id}` | Withdraw | +| `POST` | `/dictionaries/{name}/drafts/{id}/review` | Назначить ревью | +| `POST` | `/dictionaries/{name}/drafts/{id}/decision` | Approve/reject schema draft | +| `POST` | `/dictionaries/{name}/drafts/{id}/publish` | Опубликовать после approve | +| `GET` | `/drafts/me` | Мои record-drafts (status filter) | +| `GET` | `/drafts/{id}` | Один record draft | +| `DELETE` | `/drafts/{id}` | Withdraw record draft | +| `POST` | `/drafts/{id}/approve` | Одобрить (body: `{comment}`) | +| `POST` | `/drafts/{id}/reject` | Отклонить (body: `{comment}`) | +| `GET` | `/admin/reviews/pending` | Очередь ревьюера (records) | +| `GET` | `/admin/schema-reviews/pending` | Очередь ревьюера (schemas) | +| `GET` | `/admin/schema-drafts/me` | Мои schema drafts | + +## AI + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + $ORDINIS/api/v1/ai/suggest-field \ + -d '{"prompt":"контактный email оператора"}' +``` + +Limits: **30 req/min** на IP, circuit breaker **10 fails / 60s → open 5min** (`503 circuit_open`). + +## Notifications (per-user, auth) + +| Verb | Path | Что | +|---|---|---| +| `GET` | `/me/notifications` | Bell-bar feed | +| `POST` | `/me/notifications/{id}/read` | Mark as read | +| `POST` | `/me/notifications/read-all` | Mark all as read | +| `GET` | `/me/notifications/preferences` | Per-event matrix | +| `PUT` | `/me/notifications/preferences` | Сохранить матрицу | +| `GET` | `/me/notifications/settings` | Quiet hours config | +| `PUT` | `/me/notifications/settings` | Сохранить quiet hours | + +## Webhooks (admin) + +| Verb | Path | Что | +|---|---|---| +| `GET` | `/admin/webhooks/subscriptions` | Список подписок | +| `POST` | `/admin/webhooks/subscriptions` | Создать (`secret` в ответе, **один раз**) | +| `GET` | `/admin/webhooks/subscriptions/{id}` | Детали | +| `PUT` | `/admin/webhooks/subscriptions/{id}` | Обновить | +| `DELETE` | `/admin/webhooks/subscriptions/{id}` | Удалить | +| `POST` | `/admin/webhooks/subscriptions/{id}/rotate-secret` | Перевыпуск ключа | +| `POST` | `/admin/webhooks/subscriptions/{id}/test` | Test delivery | +| `GET` | `/admin/webhooks/subscriptions/{id}/stats` | Histogram (24h/7d) | +| `GET` | `/admin/webhooks/subscriptions/{id}/deliveries` | Recent deliveries | +| `GET` | `/admin/webhooks/deliveries/dlq` | DLQ list | +| `POST` | `/admin/webhooks/deliveries/{id}/retry` | Manual retry | + +### Headers delivered + +``` +X-Ordinis-Event: RecordCreated +X-Ordinis-Signature: sha256= HMAC-SHA256(body, secret) +X-Ordinis-Delivery: del_01HXY... +User-Agent: ordinis-alerts +``` + +### Event types + +`RecordCreated · RecordUpdated · RecordClosed · RecordRestored · BulkClosed · SchemaDraftSubmitted · SchemaDraftApproved · SchemaDraftRejected · SchemaDraftPublished · RecordDraftSubmitted · RecordDraftApproved` + +## Audit (admin) + +| Verb | Path | Что | +|---|---|---| +| `GET` | `/admin/audit?dictionary=&action=&user=&dateFrom=&dateTo=` | Фильтрованный аудит | +| `GET` | `/admin/audit/export.csv` | Полный экспорт | +| `GET` | `/admin/audit/export-selected.csv` | Selected ids экспорт | + +Action types (13): `DICTIONARY_CREATED · SCHEMA_UPDATED · SCHEMA_PUBLISHED · RECORD_CREATED · RECORD_UPDATED · RECORD_CLOSED · RECORD_RESTORED · DRAFT_CREATED · DRAFT_SUBMITTED · DRAFT_APPROVED · DRAFT_REJECTED · DRAFT_WITHDRAWN · REVIEWER_ASSIGNED` + +## Outbox (admin) + +| Verb | Path | Что | +|---|---|---| +| `GET` | `/admin/outbox/stats` | Pending/DLQ counters | +| `GET` | `/admin/outbox/dlq` | DLQ events list | + +## Errors — единая форма + +```json +{ "status": 422, "code": "validation_failed", "message": "...", + "details": {}, "traceId": "...", "timestamp": "..." } +``` + +| HTTP | code | | +|---|---|---| +| 400 | `bad_request` | Невалидный body | +| 401 | `unauthorized` | Нет/просрочен токен | +| 403 | `forbidden` | Scope/role не пускает | +| 404 | `not_found` | Нет такого ресурса | +| 409 | `conflict` | Concurrent update / unique violation | +| 422 | `validation_failed` | Schema validation fail | +| 429 | `rate_limited` | Лимит запросов | +| 500 | `internal_error` | См. traceId | +| 502 | `llm_error` | AI: LLM upstream | +| 503 | `circuit_open` | AI: circuit breaker | + +## Реальные примеры (staging 265) + +### FK-зависимость: кто использует справочник + +```bash +curl -s "$ORDINIS/api/v1/dictionaries/satellite_type/dependents" | jq +# [ +# { +# "sourceDict": "spacecraft", +# "sourceDisplayName": "Космические аппараты", +# "sourceField": "satellite_type_code", +# "targetField": "code", +# "onClose": "BLOCK", +# "activeRecordsInSourceDict": 14 +# } +# ] +``` + +**Семантика**: `spacecraft.satellite_type_code` ссылается на `satellite_type.code`. На staging 14 КА используют этот справочник, и `onClose: BLOCK` означает что нельзя удалить тип КА пока на него ссылаются записи (без cascade-close). + +Другие живые FK на staging: +- `ground_station` ← `antenna.ground_station` (4 records, BLOCK) +- `spacecraft_status` ← `spacecraft.status` +- `country` ← `spacecraft.country` + +### Локализация записи (Accept-Language) + +```bash +# default ru-RU +curl -s "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC" | jq '.data | {mission, operator}' +# {"mission": "Глобальный мониторинг суши и атмосферы", "operator": "НАСА"} + +# en-US +curl -sH "Accept-Language: en-US" \ + "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC" | jq '.data | {mission, operator}' +# {"mission": "Global land and atmosphere monitoring", "operator": "NASA"} +``` + +⚠️ Только Accept-Language header. `?locale=` query param backend сейчас игнорирует. Schema endpoint (`/dictionaries/{name}`) **не локализуется** вообще — всегда возвращает на defaultLocale. + +### Time-travel: запись на момент в прошлом + +```bash +# Запись на 14.05.2026 12:00 UTC (точно после её validFrom = 11:34) +curl -s "$ORDINIS/api/v1/test1/records/Dima_record_test_1?as_scope=PUBLIC&at=2026-05-14T12:00:00Z" \ + | jq '{businessKey, validFrom, validTo, data}' +# 200 OK — запись действительная на указанный момент + +# До validFrom → 404 record_not_active (это ожидаемо) +curl -s "$ORDINIS/api/v1/test1/records/Dima_record_test_1?as_scope=PUBLIC&at=2026-05-01T00:00:00Z" | jq '.code' +# "record_not_active" +``` + +### History — все ревизии записи + +```bash +curl -s "$ORDINIS/api/v1/dictionaries/spacecraft/records/1999-068A/history?as_scope=PUBLIC" | jq '.[0] | {revisionNumber, revisionType, updatedAt}' +``` + +### Schema time-travel (changelog diff) + +```bash +# Список ревизий +curl -s "$ORDINIS/api/v1/dictionaries/test1/changelog" | jq '.currentVersion, (.entries | length)' + +# Diff конкретной ревизии (before/after JSON-Schema) +curl -s "$ORDINIS/api/v1/dictionaries/test1/changelog/22/diff" | jq '{eventType, occurredAt, before: .before.properties | keys, after: .after.properties | keys}' +``` + +## 60-second smoke + +```bash +ORDINIS=https://ordinis.nstart.cloud # prod (public LE) +# или: ORDINIS=https://ordinis.k8s.265.nstart.cloud # staging + +curl -s $ORDINIS/api/v1/version | jq # alive +curl -s $ORDINIS/api/v1/dictionaries | jq 'length' # 37 на prod / 39 на staging +curl -s $ORDINIS/api/v1/dictionaries/spacecraft | jq '.scope' # "PUBLIC" +curl -s "$ORDINIS/api/v1/spacecraft/records?as_scope=PUBLIC" | jq '.[0].businessKey' +curl -s "$ORDINIS/api/v1/dictionaries/satellite_type/dependents" | jq 'length' # 1 (spacecraft зависит) +curl -s "$ORDINIS/api/v1/search?q=spacecraft" | jq '.total' # >0 +curl -s $ORDINIS/api/v1/ai/info | jq '.enabled' # true / false +``` + +## Постман collection (на случай если потерял ссылку из section 13) + +27 рабочих запросов + 10-шаговый demo flow: + +- `~/Desktop/Ordinis-Postman/` (если генерировал локально) +- [docs/integration/postman/](postman/) — версионный snapshot + +— конец приложения A —