GET /api/v1/authoring/posts — Мои посты и черновики
Авторский список постов news во всех статусах жизненного цикла (Draft/Published/Unpublished/
Archived) с keyset-пагинацией по updated_at desc — рабочая витрина автора/менеджера («мои посты и
черновики», README, область authoring). В отличие от публичной ленты
(list-global-feed.md) выдаёт неопубликованные статусы и обязан
применять record-level scope (INV-NWS-007). Только чтение: без мутаций, событий и синхронных вызовов.
Назначение
Отдать актору его собственные посты и черновики, а менеджеру компании — дополнительно посты, привязанные
к компаниям в его scope (при permission news.posts.manage), во всех статусах, с фильтром по Status и
keyset-пагинацией по времени последнего изменения.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | news |
| API area | authoring |
| Feature group | authoring |
| Статус | draft |
| Документ | docs/06-services/news/api/authoring/list-my-posts.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (автор поста / менеджер застройщика или агентства) |
| Auth policy / Permissions | JWT обязателен; news.posts.manage-own (свои посты) + news.posts.manage (посты компаний в scope менеджера); permission.md |
| Scope (RBAC) | организация — company scope менеджера (набор AuthorCompanyId из claims); свои посты — вне company scope |
| Record-level | обязателен: AuthorUserId = actor ИЛИ (AuthorCompanyId ∈ managed-scope ∧ news.posts.manage), INV-NWS-007 |
| Tenant isolation | посты инвестора/сотрудника вне tenant (AuthorCompanyId = null, ADR-0052 п.4); record-level строится явно, авто-tenant-фильтр отключён — иначе OR-условие «свои ∪ компанийные» не выразимо |
| Audit | read — отдельная audit-запись не пишется (чтение своих данных) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/authoring/posts |
| Success status | 200 |
| Idempotency header | не применяется (безопасный идемпотентный GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims | Guid | Да | из JWT; основа record-level предиката | не из тела запроса |
Status | query | PostStatus? | Нет | enum-строкой; неизвестное значение → VALIDATION_ERROR; null — все статусы | — |
Cursor | query | string? | Нет | opaque base64 keyset-курсор; при повреждении → VALIDATION_ERROR | непрозрачный, не PII |
Limit | query | int | Нет | 1..30, default 20 | защита от тяжёлых страниц |
Enum PostStatus (query, enum-строкой)
Дом значений — post.md; фильтр по одному значению Post.Status.
| Значение | Что попадает в выдачу |
|---|---|
Draft | Черновики (не в ленте/поиске). |
Published | Опубликованные (в ленте/поиске). |
Unpublished | Снятые с публикации (черновик правок сохранён). |
Archived | Soft-deleted (DeletedAt задан) — включаются в выборку authoring явным фильтром (в отличие от лент). |
Archived попадают в выдачу только при Status = Archived; при Status = null (все статусы) включение
Archived определяется явным режимом «включать удалённые» (см. алгоритм).
Модель ответа (страница AuthoringPostItem)
Постраничный список в keyset-режиме: Items + курсорные поля; TotalCount не вычисляется (перф/SLO).
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
Items | IReadOnlyList<AuthoringPostItem> | Да | projection | Страница постов, updated_at desc, id desc. |
NextCursor | string? | Нет | computed (keyset) | Opaque-курсор следующей страницы; null — конец списка. |
HasMore | bool | Да | computed | Есть ли ещё страницы. |
AuthoringPostItem
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PostId | Guid | Да | Post.Id | — | Идентификатор поста. |
Name | string | Да | Post.Name | — | Внутреннее/отображаемое имя (авторская рабочая витрина). |
Suffix | string | Да | Post.Suffix | — | URL-slug (/news/{suffix}). |
Title | string | Да | Post.Title | — | Заголовок поста. |
CategorySlug | string | Да | PostCategory.Slug | — | Стабильный kebab-код категории (SEED). |
Status | PostStatus | Да | Post.Status | — | Статус жизненного цикла (enum-строкой). |
CommentsCount | int | Да | Post.CommentsCount | — | Денормализованный счётчик комментариев. |
ViewsCount | long | Да | Post.ViewsCount | — | Денормализованные просмотры (трекинг — user-activity). |
CurrentVersionNumber | int | Да | Post.CurrentVersionNumber | — | Номер последнего PostVersion; 0 — неопубликованный черновик. |
PublishedAt | DateTimeOffset? | Нет | Post.PublishedAt | — | Момент текущей публикации; null — не публиковался/снят. |
UpdatedAt | DateTimeOffset | Да | Post.UpdatedAt | — | Последнее изменение — ключ сортировки/keyset. |
Внутренние AuthorUserId/AuthorCompanyId наружу не отдаются. Поля поста — post.md;
CategorySlug — post-category.md; история — post-version.md.
Алгоритм
- Контекст и доступ. Читает авторизованный пользователь (
User); без JWT →UNAUTHENTICATED(401). Разрешить company scope менеджера: набор компанийmanagedCompanyIds, где у актора есть правоnews.posts.manage. - Проверка входа.
Limitв диапазоне1..30(default 20);Status(если задан) — валидное значениеPostStatus;Cursor(если задан) — декодируется в keyset-позицию (LastUpdatedAt,LastId), сбой декодирования →VALIDATION_ERROR(400). - Выборка (record-level). Вернуть страницу постов, доступных актору по правилу INV-NWS-007:
AuthorUserId == ActorUserIdИЛИ (AuthorCompanyIdзадан И входит вmanagedCompanyIds). ЕслиmanagedCompanyIdsпуст — предикат сужается до собственных постов (только свои). Опционально — фильтр поStatus. Выдача включаетArchivedтолько приStatus = Archivedили в режиме «все статусы» (иначе soft-deleted скрыты). Порядок —updated_at desc, id desc(Id— Guid v7, детерминированный tiebreaker); keyset-предикат отбирает строки строго «после» позиции курсора (updated_at < LastUpdatedAt OR (updated_at = LastUpdatedAt AND id < LastId)). Читается на одну строку больше запрошенногоLimit, чтобы определить наличие следующей страницы (индексыix_posts_author/ix_posts_company, добор сортировки поupdated_at). - Маппинг и результат. Для страницы подтянуть
Slugкатегорий (SEED-справочник, кэшируемо) и собратьAuthoringPostItemполе-в-поле; внутренниеAuthorUserId/AuthorCompanyIdнаружу не отдаются. Сформировать страницу:Items= первыеLimit;HasMore= была ли лишняя строка;NextCursor— позиция последнего отданного приHasMore, иначеnull. Ответ →200. Пустая выдача — успех200с пустымItems.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (read-only) |
| External calls | none — категории из локальной таблицы, без синхронного identity/selections |
| Cache / projections / search | Чтение ix_posts_author/ix_posts_company; SEED PostCategory кэшируемо; собственного кэша ответа нет (ADR-0056) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
UNAUTHENTICATED | 401 | нет/невалидный JWT | «Требуется авторизация» | no retry |
VALIDATION_ERROR | 400 | Limit вне 1..30; неизвестный Status; повреждённый Cursor | «Проверьте параметры запроса» | no retry |
Отказ БД — инфраструктурная ошибка (5xx, ProblemDetails), а не бизнес-ошибка. Пустая выдача (нет
постов актора под фильтр) — успех 200 с пустым Items, не ошибка. Менеджер без company scope видит
только свои посты — не ошибка (предикат сужается).
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор аутентифицирован (
User); для доступа к постам компаний —news.posts.manageв scope этих компаний; - запрошенный
Status(если задан) — валидное значениеPostStatus;Cursor(если задан) — корректный keyset-курсор.
Постусловия (гарантированы после чтения):
- состояние не изменяется (read-only): нет мутаций, событий и синхронных вызовов;
- выдача содержит только посты, доступные актору по record-level INV-NWS-007 (свои
AuthorUserId∪ посты компаний в его managed-scope) — чужие посты вне scope не попадают; Archivedвключаются только приStatus = Archivedлибо в режиме «все статусы»; порядок —updated_at desc, id desc, keyset-страница ≤Limit,NextCursor/HasMoreсогласованы; свежесть — актуальная на момент запроса (кэша нет).
Acceptance Criteria
- Свои посты во всех статусах. Дано: автор с постами
Draft/Published/Unpublished. Когда:GET /api/v1/authoring/postsбезStatus. Тогда:200, страница своих постов в порядкеupdated_at desc,Archivedскрыты (если не режим «все статусы»). - Фильтр по статусу. Дано:
Status = Draft. Когда:GET ...?status=Draft. Тогда: только черновики актора. - Явный запрос архива. Дано:
Status = Archived. Когда:GET ...?status=Archived. Тогда: soft-deleted посты актора включены. - Company scope менеджера. Дано: менеджер с
news.posts.manageнад компанией. Когда:GET. Тогда: выдача = свои посты ∪ посты компаний в managed-scope (INV-NWS-007). - Менеджер без scope. Дано: у актора нет managed-компаний. Когда:
GET. Тогда: только собственные посты (предикат сужается — не ошибка). - Изоляция. Дано: чужой пост вне scope актора. Когда:
GET. Тогда: пост не появляется в выдаче. - Keyset-пагинация. Дано: постов больше
Limit. Когда:GETсNextCursorпредыдущей страницы. Тогда: следующая страница без пропусков/дублей,HasMoreкорректен. - Пустая выдача. Дано: у актора нет постов под фильтр. Когда:
GET. Тогда:200с пустымItems(не ошибка). - Невалидные параметры. Дано:
Limitвне1..30, неизвестныйStatusили повреждённыйCursor. Когда:GET. Тогда:400 VALIDATION_ERROR. - Гость. Дано: нет/невалидный JWT. Когда:
GET. Тогда:401 UNAUTHENTICATED.
Совместимость и наблюдаемость
- Новые optional-поля
AuthoringPostItem/значенияPostStatus— additive (без breaking). Удаление/ переименование параметра или поля, смена типа — breaking (versioning + rollout). НеизвестныйStatusв запросе →VALIDATION_ERROR(fail-fast, не тихий fallback). - Logs:
authoring posts queried(actorUserId,status,managedScopeSize,resultCount; без PII). Metrics:news_authoring_list_requests_total{status,result}, latency p99. Traces — сквозной trace черезtraceparent; dashboardnews-overview. - SLO: p99 < 300 мс (keyset +
ix_posts_author/ix_posts_company). Тесты record-level изоляции обязательны (authoring, ADR-0052): актор не видит чужие посты вне своего company scope.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-NWS-006. История версий поста (полный snapshot)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности/проекции: post.md (поля,
PostStatus, INV-NWS-007 record-level,ix_posts_author/ix_posts_company), post-category.md (Slug), post-version.md (CurrentVersionNumber). - Соседний endpoint: list-global-feed.md (публичная лента, keyset); карта области — README.md.
- Доступ: permission.md (
news.posts.manage-own/manage); ADR-0052 (tenancy), ADR-0056 (масштаб/индексы).