Перейти к основному содержимому

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.

Metadata

ПолеЗначение
Сервис/контекстselections
API areainternal
Feature groupsnapshots
Статусdraft
Документdocs/06-services/selections/api/internal/get-selection-snapshot.md

Актор и доступ

ПроверкаЗначение
Actor typeExternalSystem — m2m-клиент news (client-credentials), INV-SNP-003
Auth policy / Permissionsauthorization policy InternalNewsOnly (m2m-клиент = news); пользовательского пути к операции нет → 403 для остальных
Scope (RBAC)не применяется
Record-levelне применяется: news — доверенный вызыватель; сам пост публичен, snapshot — его контент
Tenant isolationне применяется — вне tenant (ADR-0052 п.3: контент публичного поста)
Auditnone (безопасное чтение; OwnerUserId наружу не отдаётся — отображение автора поста решает news; PII в контенте нет)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/internal/snapshots/{id:guid}
Success status200
Idempotency headerне применяется — безопасный read, состояние не меняется
Correlationсквозной trace через заголовок traceparent, из news

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
SnapshotIdroute ({id})GuidДа!= Guid.Empty; snapshot существует (GetById)

Body отсутствует.

Модель ответа SelectionSnapshotContentResult

ПолеТипОбязательностьИсточникMaskingОписание
SnapshotIdGuidДаSelectionSnapshot.IdEcho {id}.
SourceSelectionIdGuidДаSelectionSnapshot.SourceSelectionIdПодборка-источник.
PostIdGuidДаSelectionSnapshot.PostIdПост, в блок которого встроен snapshot.
Statusstring (enum SnapshotStatus)ДаSelectionSnapshot.StatusActive / Detached; отдаётся в обоих (INV-SNP-006).
ContentSelectionSnapshotContentДаSelectionSnapshot.Content (jsonb)Замороженный слепок; поля ниже.
DetachedAtDateTimeOffset?НетSelectionSnapshot.DetachedAtЗаполнено при Detached.
DetachReasonstring? (enum SnapshotDetachReason)НетSelectionSnapshot.DetachReasonBlockReplaced / PostDeleted; при Detached.

OwnerUserId в ответ не включается (аудит-поле; автора поста показывает news).

Вложенная модель SelectionSnapshotContent (ContractVersion = 1)

Проекция замороженного jsonb; канонический дом — ../domain/selection-snapshot.md.

ПолеТипОбязательностьОграниченияОписание
ContractVersionintДа= 1Версия схемы контента.
TitlestringДа1..256Копия Selection.Title на момент снятия.
Descriptionstring?Нет≤ 2000Копия Selection.Description.
SourceSelectionPublicCodestringДа12 base62Ссылка «открыть живую подборку».
ItemsIReadOnlyList<SelectionSnapshotItem>Да1..200, в порядке SortOrderСостав слепка.
TakenAtDateTimeOffsetДаМомент снятия слепка.

Вложенная модель SelectionSnapshotItem

ПолеТипОбязательностьОграниченияОписание
PropertyObjectIdGuidДаdisplay-данные добирает рендер батчем в catalogОбъект недвижимости элемента.
SortOrderintДа>= 0Порядок в подборке.
Commentstring?Нет≤ 4000Комментарий владельца (публичен вместе с постом).

Алгоритм

  1. Контекст и доступ. Доверенный m2m news (client-credentials); вызыватель не news403 (INV-SNP-003).
  2. Проверка входа и предусловий. SnapshotId непустой, иначе VALIDATION_ERROR.
  3. Что читаем. Snapshot по id (jsonb Content целиком, без GIN — поиск по содержимому не нужен). Отсутствие → SELECTIONS_SNAPSHOT_NOT_FOUND (404). Фильтра по статусу нет — Detached тоже отдаётся (INV-SNP-006).
  4. Бизнес-правила и маппинг. Маппинг поле-в-поле: SnapshotId/SourceSelectionId/PostId/ Status/DetachedAt/DetachReason — из snapshot-а; Content — целиком (Title, Description, SourceSelectionPublicCode, Items в порядке SortOrder, TakenAt). Читатель обязан поддерживать все ContractVersion, встречающиеся в БД, unknown-поля игнорирует (snapshot-ы иммутабельны и не мигрируются, INV-SEL-001). OwnerUserId в результат не переносится.
  5. Результат. Модель SelectionSnapshotContentResult → HTTP 200.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400SnapshotId == Guid.Empty«Некорректный запрос»no retry
SELECTIONS_SNAPSHOT_NOT_FOUND404Snapshot не существует«Snapshot не найден»no retry
FORBIDDEN403Вызыватель не m2m news (INV-SNP-003)no retry

Сетевой сбой/5xx: безопасно повторять (read-only, без побочных эффектов).

Кросс-сервисный контракт (selectionsnews)

  • 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).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы