# 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=" ``` Ответ: ```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=` (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`.