Files
mdm-ordinis/docs-internal/tech/api-integration.md
T
Zimin A.N. bcfb07f547 docs: split integrator guide (Diplodoc) vs internal docs + add build-docs CI
Чёткое разделение audiences:
- docs/ — consumer-facing only, Diplodoc-built. Public contract для
  интеграторов (Альтум, BI, third-party).
- docs-internal/ — operator runbooks, status snapshots, legacy tech pages.
  НЕ в Diplodoc build, НЕ публикуются.

Изменения:
- Move docs/{ops,status,tech} → docs-internal/{ops,status,tech}
- docs/toc.yaml: убраны секции Operations, Архитектура (legacy), Status
- docs/index.md: убран tab "оператор / on-call", focus на integrators
- docs/README.md: rewritten — scope только integrator guide, editorial
  guidelines (no leak internal architecture, stable API contract)
- .yfm: убран явный ignore status/* (теперь не нужно — папка переехала)
- docs-internal/README.md: index для внутренней документации с rationale
  разделения

Scrubbing implementation details из Diplodoc страниц:
- events.md: убраны Strimzi/cluster.142 references, OutboxPoller детали,
  internal alert names (OrdinisOutboxLagHigh, OrdinisOutboxDLQNonEmpty),
  metric names (nsi_outbox_*). Заменены на consumer-observable contract.
- webhooks.md: убраны metric names (nsi_webhook_ssrf_rejected_total),
  alert table (OrdinisWebhookHighFailureRate и т.д.). Retry policy
  переписана на user-side perspective.
- errors.md: убран alert name OrdinisReadApiErrorBudgetBurnFast, Tempo
  endpoint URL, internal connection details.
- x-references.md: OrphanReferenceScanner → general "Ordinis периодически
  детектирует". Убраны metric/alert names. Cascade roadmap без link на
  internal design doc.
- auth.md: убран file path ScopeContext.java.
- bundle-cuod.md: убран Maven module path.
- caching.md: cascade design link заменён на text reference.

CI:
- .gitlab-ci.yml: новый job build-docs (stage: build) — Node 20 alpine,
  pnpm install --frozen-lockfile + pnpm build, artifact docs/_dist на
  неделю. Triggers: auto на docs/**, либо manual web. Готов к follow-up
  pages-deploy job когда host решён.
- Новый pattern .docs-changes для rules.

Build verified clean: 13 HTML pages, 6.9M, no broken links/refs.
2026-05-07 23:27:03 +03:00

260 lines
12 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.
# Ordinis API — гайд для интеграторов
Документ для команд которые хотят читать/писать справочники Ordinis из своих
сервисов (Альтум, геопортал, BI и т.д.).
**Owner:** Ordinis team — zimin.an@nstart.space.
## 1. Где живёт сервис
| Среда | URL | Auth |
|---------|--------------------------------------------------|-------------------------------------------|
| staging | `https://ordinis.k8s.265.nstart.cloud` | пока выключена (`ORDINIS_AUTH_REQUIRED=false`) |
| prod | `https://ordinis.k8s.264.nstart.cloud` | будет JWT (после prod stand-up) |
| local | `http://localhost:8080` | выключена |
## 2. Swagger UI / OpenAPI spec
- **Swagger UI:** `https://ordinis.k8s.265.nstart.cloud/swagger-ui.html`
- **Spec JSON:** `https://ordinis.k8s.265.nstart.cloud/v3/api-docs`
- **Spec YAML:** `https://ordinis.k8s.265.nstart.cloud/v3/api-docs.yaml`
Из spec можно генерировать клиент для Python (openapi-generator-cli),
TypeScript, Go и т.д. Пример:
```bash
openapi-generator-cli generate \
-i https://ordinis.k8s.265.nstart.cloud/v3/api-docs \
-g python \
-o ./altum-backend/clients/ordinis
```
## 3. Авторизация
Ordinis принимает JWT от Keycloak realm `nstart`:
- Issuer: `https://auth.nstart.space/realms/nstart`
- Алгоритм: RS256
- JWK Set discoverится через `/.well-known/openid-configuration`.
### Service account (m2m)
Для интеграции backend↔backend (например altum-backend → ordinis read-api)
получить service account клиент в Keycloak:
1. Запросить у Ordinis team создание клиента в realm `nstart`.
2. Назначить роль `ordinis-public` (или выше, если нужен RESTRICTED scope).
3. Получить `client_id` + `client_secret`.
**Получение access token (client_credentials):**
```bash
curl -X POST https://auth.nstart.space/realms/nstart/protocol/openid-connect/token \
-d "grant_type=client_credentials" \
-d "client_id=altum-backend" \
-d "client_secret=<SECRET>"
```
Ответ:
```json
{ "access_token": "eyJhbGciOi...", "expires_in": 300, "token_type": "Bearer" }
```
Кэшировать токен с TTL = `expires_in - 30` (буфер на retries) в backend, не
держать в фронте.
### Маппинг ролей в DataScope
| Keycloak роль | Ordinis DataScope |
|-------------------------------------------------|-------------------|
| `ordinis-public` / `public` | PUBLIC |
| `ordinis-internal` / `internal` | PUBLIC + INTERNAL |
| `ordinis-restricted` / `restricted` | + RESTRICTED |
Любая роль с суффиксом `-PUBLIC|INTERNAL|RESTRICTED` тоже работает (см.
`ordinis-auth/.../ScopeContext.java`). Это сделано чтобы не плодить orgaroles
для каждого consumer'а.
## 4. Read-side endpoints
### `GET /api/v1/dictionaries`
Список доступных справочников.
```bash
curl -H "Authorization: Bearer $TOKEN" \
https://ordinis.k8s.265.nstart.cloud/api/v1/dictionaries
```
```json
[
{ "name": "spacecraft", "displayName": "Космические аппараты", "scope": "PUBLIC", "schemaVersion": "1.0.0" },
{ "name": "mission", "displayName": "Миссии / программы", "scope": "PUBLIC", "schemaVersion": "1.0.0" },
...
]
```
### `GET /api/v1/{name}/records`
> ⚠️ **БЕЗ префикса `/dictionaries/`!** Read-api сидит на `/api/v1/{name}/records`.
> `/api/v1/dictionaries/...` — это admin-write контроллер для CRUD, у него нет
> bulk-list эндпоинта, не пытайтесь его читать.
Активные записи на момент `at` (default: `now`). Параметры:
| параметр | тип | пример | примечание |
|----------|-----------|------------------------------|------------------------------------------------|
| `lang` | string | `ru-RU`, `en-US` | flatten i18n полей до строки на этом языке. Короткие формы (`ru`, `en`) тоже принимаются. |
| `at` | ISO 8601 | `2026-05-04T12:00:00+03:00` | bitemporal query — вернёт активные на момент |
| `as_scope` | string | `PUBLIC,INTERNAL` | для anonymous + `allowQueryScope=true` (legacy) |
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://ordinis.k8s.265.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU"
```
Без `?lang` ответ возвращает локализованные поля как объект:
```json
"name": { "ru-RU": "Ресурс-П № 3", "en-US": "Resurs-P No.3" }
```
С `?lang=ru-RU`:
```json
"name": "Ресурс-П № 3"
```
### `GET /api/v1/{name}/records/{businessKey}`
Одна запись. Параметры те же.
### `GET /api/v1/{name}/records/{businessKey}/history`
Все версии записи (включая закрытые). Используется UI-историей, для consumer'ов
обычно не нужно.
## 5. Стратегии кэширования
### A) Простой TTL (рекомендуется для v1)
```ts
// TanStack Query
useQuery({
queryKey: ['ordinis', 'spacecraft', lang],
queryFn: () => fetch(`/api/reference/spacecraft?lang=${lang}`).then(r => r.json()),
staleTime: 5 * 60_000, // 5 минут — справочники меняются редко
retry: 3,
})
```
### B) Webhook / Kafka push (v2)
Когда нужны realtime инвалидации — подписаться на топик
`nsi.dictionary.changed` (планируется в Ordinis v2). Payload:
```json
{
"name": "spacecraft",
"businessKey": "RESURS-P-3",
"action": "updated", // created / updated / closed
"at": "2026-05-15T10:30:00+03:00"
}
```
Consumer вычищает соответствующий ключ из своего кэша и pull'ит свежую версию.
### C) Snapshot at order time (для аудит-кейсов)
Когда нужен «как было на момент заказа» — сохранить полную копию использованных
справочников рядом с заказом:
```sql
CREATE TABLE imaging_order (
id UUID PRIMARY KEY,
spacecraft_business_key VARCHAR(256),
instrument_business_key VARCHAR(256),
reference_snapshot JSONB, -- копии записей, не FK
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
Альтернатива — запрашивать read-api с `?at=<order.created_at>` (bitemporal
поддерживает это). Snapshot надёжнее: read-only к prod, не зависит от наличия
старых версий после retention.
## 6. Best practices
1. **Не делайте FK на копию справочника**. Храните `business_key VARCHAR`
это стабильный идентификатор, валидный сквозь версии.
2. **Кэшируйте per `(dictionary, lang)`**. Inline в коде — `staleTime`/TTL.
3. **Refresh JWT перед TTL**. Не дожидайтесь 401 — большинство Keycloak SDK
рефрешит автоматически.
4. **Stale cache на 5xx/timeout**. Если Ordinis недоступен, отдавайте старые
данные с warn'ом в лог. Не падайте каскадно.
5. **Используйте businessKey, не UUID**. UUID — внутренний идентификатор записи
и меняется при создании новой версии (через update). businessKey стабилен.
6. **Не пишите массовых update'ов**. Ordinis оптимизирован под редкие записи и
частые чтения. Bulk import через bundle (`POST /admin/bundle/import`) или
через ETL.
## 7. Bitemporal модель — что значит `validFrom` / `validTo`
Каждая запись имеет:
- `validFrom` — с какого момента запись активна (бизнес-время).
- `validTo` — до какого момента (или `9999-12-31` если бессрочно).
- `version` — порядковый номер версии (JPA optimistic lock).
При update создаётся **новая версия**: старая `validTo` ставится в `now`,
новая создаётся с `validFrom = now`. Запись по `businessKey` всегда активна на
любой момент времени (не считая периодов когда она закрыта).
Для consumer'ов это значит: `GET /records/{key}?at=X` всегда возвращает
актуальную версию (или 404 если `key` был закрыт на этот момент).
## 8. Текущие справочники в bundle `cuod` (anchor v1)
| Имя | Scope | Записей (на staging) | Назначение |
|--------------------|--------|----------------------|-----------------------------------------------|
| `spacecraft` | PUBLIC | 4 | Каталог КА |
| `satellite_type` | PUBLIC | 5 | Классификация типов |
| `ground_station` | PUBLIC | 3 | Наземные пункты |
| `mission` | PUBLIC | 9 | Программы (Ресурс-П, Канопус, Sentinel...) |
| `instrument` | PUBLIC | 10 | Бортовые сенсоры (Геотон, MODIS, C-SAR...) |
| `processing_level` | PUBLIC | 8 | Уровни L0-L4 |
| `data_format` | PUBLIC | 8 | GeoTIFF, NITF, SAFE и т.д. |
Все справочники имеют `i18n` поля (`name`, `description`) с RU + EN. Используйте
`?lang=ru` для русского flatten, `?lang=en` для английского.
## 9. Cross-references между справочниками
- `spacecraft.satellite_type_code``satellite_type.code`
- `mission.spacecraft_codes``spacecraft.businessKey[]`
- `instrument.spacecraft_codes``spacecraft.businessKey[]`
- `instrument.supported_format_codes``data_format.businessKey[]`
- `instrument.supported_level_codes``processing_level.businessKey[]`
Эти связи **не enforced** на уровне БД (это NoSQL-style references поверх
JSONB). Consumer должен сам подтягивать связанные записи параллельными
запросами либо JOIN'ить в своей БД через snapshot.
Cascading select для UI:
```ts
const instrument = ... // выбран MSI-S2
const formats = await fetch(`/dictionaries/data_format/records?lang=${lang}`)
.then(r => r.json())
const allowedFormats = formats.filter(f =>
instrument.data.supported_format_codes.includes(f.businessKey))
// allowedFormats: SAFE, GEOTIFF, COG, JPEG2000
```
## 10. Поддержка / эскалация
- Bug в API: `git@git.nstart.local:2-6/2-6-4/terravault/ordinis` issues.
- Запрос новой роли в Keycloak / нового scope: zimin.an@nstart.space.
- Запрос нового справочника / схемы: создать issue в репо ordinis с label `dictionary-request`.