# 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`.
