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

12 KiB
Raw Blame History

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 и т.д. Пример:

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

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

Ответ:

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

Список доступных справочников.

curl -H "Authorization: Bearer $TOKEN" \
  https://ordinis.k8s.265.nstart.cloud/api/v1/dictionaries
[
  { "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)
curl -H "Authorization: Bearer $TOKEN" \
  "https://ordinis.k8s.265.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU"

Без ?lang ответ возвращает локализованные поля как объект:

"name": { "ru-RU": "Ресурс-П № 3", "en-US": "Resurs-P No.3" }

С ?lang=ru-RU:

"name": "Ресурс-П № 3"

GET /api/v1/{name}/records/{businessKey}

Одна запись. Параметры те же.

GET /api/v1/{name}/records/{businessKey}/history

Все версии записи (включая закрытые). Используется UI-историей, для consumer'ов обычно не нужно.

5. Стратегии кэширования

A) Простой TTL (рекомендуется для v1)

// 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:

{
  "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 (для аудит-кейсов)

Когда нужен «как было на момент заказа» — сохранить полную копию использованных справочников рядом с заказом:

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_codesatellite_type.code
  • mission.spacecraft_codesspacecraft.businessKey[]
  • instrument.spacecraft_codesspacecraft.businessKey[]
  • instrument.supported_format_codesdata_format.businessKey[]
  • instrument.supported_level_codesprocessing_level.businessKey[]

Эти связи не enforced на уровне БД (это NoSQL-style references поверх JSONB). Consumer должен сам подтягивать связанные записи параллельными запросами либо JOIN'ить в своей БД через snapshot.

Cascading select для UI:

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.