GET /api/v1/internal/snapshots/{id} — Контент snapshot-а для рендера поста (m2m news)
Чтение замороженного слепка подборки (SelectionSnapshot) по SnapshotId для рендера блока-подборки
опубликованного поста news. Read-only: состояние не меняется. Отдаётся и для Active, и для
Detached (история версий поста в news ссылается на отвязанные snapshot-ы, INV-SNP-006).
Назначение
news при рендере опубликованного поста берёт SnapshotId из typed-jsonb блока поста и запрашивает
этот метод, чтобы получить неизменяемый состав подборки на момент публикации (INV-SEL-001).
Display-данные объектов недвижимости (PropertyObjectId) рендер добирает батчем в catalog — в слепок они не заморожены.
SourceSelectionPublicCode даёт посту ссылку «открыть живую подборку». Канонический дом полей и
модели Content — ../domain/selection-snapshot.md.
| Поле | Значение |
|---|
| Сервис/контекст | selections |
| API area | internal |
| Feature group | snapshots |
| Статус | draft |
| Документ | docs/06-services/selections/api/internal/get-selection-snapshot.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ExternalSystem — m2m-клиент news (client-credentials), INV-SNP-003 |
| Auth policy / Permissions | authorization policy InternalNewsOnly (m2m-клиент = news); пользовательского пути к операции нет → 403 для остальных |
| Scope (RBAC) | не применяется |
| Record-level | не применяется: news — доверенный вызыватель; сам пост публичен, snapshot — его контент |
| Tenant isolation | не применяется — вне tenant (ADR-0052 п.3: контент публичного поста) |
| Audit | none (безопасное чтение; OwnerUserId наружу не отдаётся — отображение автора поста решает news; PII в контенте нет) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/internal/snapshots/{id:guid} |
| Success status | 200 |
| Idempotency header | не применяется — безопасный read, состояние не меняется |
| Correlation | сквозной trace через заголовок traceparent, из news |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
SnapshotId | route ({id}) | Guid | Да | != Guid.Empty; snapshot существует (GetById) | — |
Body отсутствует.
Модель ответа SelectionSnapshotContentResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
SnapshotId | Guid | Да | SelectionSnapshot.Id | — | Echo {id}. |
SourceSelectionId | Guid | Да | SelectionSnapshot.SourceSelectionId | — | Подборка-источник. |
PostId | Guid | Да | SelectionSnapshot.PostId | — | Пост, в блок которого встроен snapshot. |
Status | string (enum SnapshotStatus) | Да | SelectionSnapshot.Status | — | Active / Detached; отдаётся в обоих (INV-SNP-006). |
Content | SelectionSnapshotContent | Да | SelectionSnapshot.Content (jsonb) | — | Замороженный слепок; поля ниже. |
DetachedAt | DateTimeOffset? | Нет | SelectionSnapshot.DetachedAt | — | Заполнено при Detached. |
DetachReason | string? (enum SnapshotDetachReason) | Нет | SelectionSnapshot.DetachReason | — | BlockReplaced / PostDeleted; при Detached. |
OwnerUserId в ответ не включается (аудит-поле; автора поста показывает news).
Вложенная модель SelectionSnapshotContent (ContractVersion = 1)
Проекция замороженного jsonb; канонический дом — ../domain/selection-snapshot.md.
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|
ContractVersion | int | Да | = 1 | Версия схемы контента. |
Title | string | Да | 1..256 | Копия Selection.Title на момент снятия. |
Description | string? | Нет | ≤ 2000 | Копия Selection.Description. |
SourceSelectionPublicCode | string | Да | 12 base62 | Ссылка «открыть живую подборку». |
Items | IReadOnlyList<SelectionSnapshotItem> | Да | 1..200, в порядке SortOrder | Состав слепка. |
TakenAt | DateTimeOffset | Да | — | Момент снятия слепка. |
Вложенная модель SelectionSnapshotItem
| Поле | Тип | Обязательность | Ограничения | Описание |
|---|
PropertyObjectId | Guid | Да | display-данные добирает рендер батчем в catalog | Объект недвижимости элемента. |
SortOrder | int | Да | >= 0 | Порядок в подборке. |
Comment | string? | Нет | ≤ 4000 | Комментарий владельца (публичен вместе с постом). |
Алгоритм
- Контекст и доступ. Доверенный m2m
news (client-credentials); вызыватель не news → 403
(INV-SNP-003).
- Проверка входа и предусловий.
SnapshotId непустой, иначе VALIDATION_ERROR.
- Что читаем. Snapshot по id (jsonb
Content целиком, без GIN — поиск по содержимому не нужен).
Отсутствие → SELECTIONS_SNAPSHOT_NOT_FOUND (404). Фильтра по статусу нет — Detached тоже
отдаётся (INV-SNP-006).
- Бизнес-правила и маппинг. Маппинг поле-в-поле:
SnapshotId/SourceSelectionId/PostId/
Status/DetachedAt/DetachReason — из snapshot-а; Content — целиком (Title, Description,
SourceSelectionPublicCode, Items в порядке SortOrder, TakenAt). Читатель обязан поддерживать
все ContractVersion, встречающиеся в БД, unknown-поля игнорирует (snapshot-ы иммутабельны и не
мигрируются, INV-SEL-001). OwnerUserId в результат не переносится.
- Результат. Модель
SelectionSnapshotContentResult → HTTP 200.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_ERROR | 400 | SnapshotId == Guid.Empty | «Некорректный запрос» | no retry |
SELECTIONS_SNAPSHOT_NOT_FOUND | 404 | Snapshot не существует | «Snapshot не найден» | no retry |
FORBIDDEN | 403 | Вызыватель не m2m news (INV-SNP-003) | — | no retry |
Сетевой сбой/5xx: безопасно повторять (read-only, без побочных эффектов).
Кросс-сервисный контракт (selections ↔ news)
selections ожидает от news: валидный m2m токен (policy InternalNewsOnly); SnapshotId,
который news хранит в typed-jsonb блоке поста (получен из
create-selection-snapshot.md).
selections предоставляет news: замороженный Content (ContractVersion = 1) — состав на
момент публикации (INV-SEL-001), плюс Status и SourceSelectionPublicCode (ссылка «открыть живую
подборку»). OwnerUserId наружу не отдаётся — подпись автора решает news. Snapshot отдаётся и в
Active, и в Detached (история версий поста ссылается на отвязанные, INV-SNP-006).
- Поведение при устаревании версии контента: читатель обязан поддерживать все
ContractVersion,
встречающиеся в БД, unknown-поля игнорировать — snapshot-ы иммутабельны и не мигрируются (INV-SEL-001).
Предусловия и постусловия
- Предусловия: вызыватель — доверенный m2m
news; SnapshotId непустой.
- Постусловия: состояние не меняется (безопасное чтение); при существующем snapshot-е возвращается
его
Content целиком (независимо от Status), display-данные объектов в слепок не заморожены.
Acceptance Criteria
- Given
Active-snapshot, When news вызывает GET /internal/snapshots/{id}, Then 200 с Content
(Title, Items в порядке SortOrder, SourceSelectionPublicCode, TakenAt), Status = Active,
без OwnerUserId.
- Given
Detached-snapshot (блок заменён / пост удалён), When GET, Then 200 с тем же замороженным
Content (INV-SNP-006), Status = Detached, заполнены DetachedAt/DetachReason — история версий
поста продолжает рендериться.
- Given подборка-источник после снятия была переименована/изменена, When
GET snapshot-а, Then контент
остаётся прежним (INV-SEL-001) — правка живой подборки на слепок не влияет.
- Given
SnapshotId, которого нет, When GET, Then 404 SELECTIONS_SNAPSHOT_NOT_FOUND.
- Given вызыватель не
news, When GET, Then 403 FORBIDDEN (INV-SNP-003).
- Given
SnapshotId == Guid.Empty, When GET, Then 400 VALIDATION_ERROR.
- Given snapshot с
Content.ContractVersion, которую сервис знает как старую, When GET, Then контент
возвращается как есть, unknown-поля игнорируются (snapshot не мигрируется).
Совместимость и наблюдаемость
- Reader толерантен к версиям: поддерживает все
ContractVersion контента в БД, unknown-поля
игнорирует; snapshot-ы не мигрируются (INV-SEL-001). Новые optional-поля результата — additive;
удаление/переименование/смена типа — breaking (migration + rollout).
- Logs:
SnapshotId, Status; metrics: selections_snapshot_read_total; traces: сквозной span из
news. Alert: рост 5xx internal-области блокирует рендер постов news (../README.md).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы