PUT /api/v1/posts/{id} — обновить метаданные поста
Authoring-точка правки метаданных поста ленты новостей. Поля агрегата — дом
post.md, справочник категорий — дом post-category.md.
Правит только метаданные (name/suffix/title/subtitle/categoryId/projectRefs); блоки контента —
отдельная операция PUT /api/v1/posts/{id}/blocks, публикация — POST /api/v1/posts/{id}/publish
(карта — README).
Назначение
Позволить автору (или менеджеру компании-автора) обновить редактируемые метаданные существующего поста
в статусе Draft/Unpublished/Published без смены статуса и без записи PostVersion: слепок истории
пишется только при (re)publish (INV-NWS-005, post-version.md INV-PV-004).
Категория ревалидируется на eligibility и атрибуцию, суффикс — на уникальность, SearchText
пересчитывается при изменении текстовых полей (INV-NWS-008).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | news |
| API area | authoring |
| Feature group | authoring |
| Статус | draft |
| Документ | docs/06-services/news/api/authoring/update-post-draft.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (аутентифицированный автор; guest — 401) |
| Auth policy / Permissions | JWT identity; news.posts.manage-own (self-scope автора) либо news.posts.manage (менеджер компании/эскалация). Каталог — identity/domain/permission.md |
| Scope (RBAC) | self + company: правит AuthorUserId = actor.UserId либо менеджер в scope AuthorCompanyId |
| Record-level | INV-NWS-007: мутация разрешена только автору, менеджеру компании в scope AuthorCompanyId или носителю news.posts.manage; иначе 403 |
| Tenant isolation | TenantId = Post.AuthorCompanyId (nullable, ADR-0052 п.4); посты инвестора/сотрудника — вне tenant, гейт по record-level AuthorUserId |
| Audit | update: UpdatedByUserId = actor.UserId, UpdatedAt, инкремент доменного Version; PII нет |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/v1/posts/{id} |
| Success status | 200 |
| Idempotency header | не применяется (PUT семантически идемпотентен; повтор перезаписывает те же поля) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Единая секция: HTTP-источник + типизированное поле request-модели UpdatePostCommand.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
Id | route (id) | Guid | Да | != Guid.Empty; существующий неудалённый Post (шаг 2) | record-level (шаг 2) |
Name | body | string | Да | 1..256 после Trim | индексируется в поиск |
Suffix | body | string | Да | 1..128, kebab-case ^[a-z0-9]+(-[a-z0-9]+)*$; уникален среди неудалённых, исключая себя (шаг 3) | URL-slug (INV-NWS-001) |
Title | body | string | Да | 1..512 после Trim | индексируется в поиск |
Subtitle | body | string? | Нет | <= 1024 | индексируется в поиск |
CategoryId | body | Guid | Да | != Guid.Empty; Active-категория + eligibility (шаг 3) | — |
ProjectRefs | body | PostProjectRefs | Нет | ProjectIds: уникальны, count <= 20; непустой — только AuthorType = DeveloperManager (INV-NWS-003) | логические ссылки (без FK) |
ActorUserId | identity claims | Guid | Да | из JWT актора | стамп, не из тела |
ProjectRefs — именованный контейнер привязок к проектам (дом — post.md).
Модель ответа PostResult
Проекция полей агрегата Post (дом — post.md); ниже — дельта состава ответа.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | Post.Id | — | Идентификатор поста. |
AuthorUserId | Guid | Да | Post.AuthorUserId | — | Автор (не меняется правкой). |
AuthorType | PostAuthorType (enum строкой) | Да | Post.AuthorType | — | Тип автора (зафиксирован при создании). |
AuthorCompanyId | Guid? | Нет | Post.AuthorCompanyId | — | Компания = TenantId; null вне tenant. |
CategoryId | Guid | Да | Post.CategoryId | — | Актуальная категория. |
Name | string | Да | Post.Name | — | Обновлённое название. |
Suffix | string | Да | Post.Suffix | — | Обновлённый URL-slug. |
Title | string | Да | Post.Title | — | Обновлённый заголовок. |
Subtitle | string? | Нет | Post.Subtitle | — | Обновлённый подзаголовок. |
ProjectRefs | PostProjectRefs | Нет | Post.ProjectRefs | — | Привязка к проектам (container-VO). |
CoverFileId | Guid? | Нет | Post.CoverFileId | — | Обложка (правится в blocks-операции). |
Status | PostStatus (enum строкой) | Да | Post.Status | — | Не меняется этой операцией. |
CommentsEnabled | bool | Да | Post.CommentsEnabled | — | Флаг комментариев. |
CurrentVersionNumber | int | Да | Post.CurrentVersionNumber | — | Не инкрементируется правкой (0 для неопубликованных). |
PublishedAt | DateTimeOffset? | Нет | Post.PublishedAt | — | Не меняется. |
UpdatedAt | DateTimeOffset | Да | Post.UpdatedAt | — | Момент этой правки. |
Version | long | Да | Post.Version | — | Доменная версия после инкремента. |
Алгоритм
- Контекст и доступ. Правит автор поста или менеджер компании-автора (
User); гость →401. Требуетсяnews.posts.manage-own(self) либоnews.posts.manage(менеджер компании/эскалация).PUTидемпотентен: повтор перезаписывает те же поля. - Проверка входа и доступа. Форматы и длины
Name/Suffix/Title/Subtitle, kebab-паттернSuffix,CategoryIdзадан,ProjectRefs.ProjectIdsуникальны и не более 20; нарушение →VALIDATION_FAILED. Найти пост поId; если не найден илиArchived(soft-deleted) →NEWS_POST_NOT_FOUND. Редактируемы статусыDraft/Unpublished/Published; правкаPublishedне меняет статус и не пишетPostVersion(INV-NWS-005 / INV-PV-004 — версия только при (re)publish). Record-level (INV-NWS-007): актор — автор, либо менеджер компании в scopeAuthorCompanyId, либо носительnews.posts.manage; иначе →FORBIDDEN(403). - Ревалидация категории и суффикса. Найти категорию по
CategoryId; если не найдена →NEWS_CATEGORY_NOT_FOUND. eligibility (INV-NWS-002 / INV-CAT-002): категорияActiveиAuthorTypeвходит вAllowedAuthorTypes; иначе →NEWS_CATEGORY_NOT_ALLOWED. Атрибуция (INV-NWS-003): непустойProjectRefsдопустим только приAuthorType = DeveloperManager; иначе →NEWS_ATTRIBUTION_NOT_ALLOWED. Суффикс должен быть свободен среди неудалённых постов (кроме самого поста), иначе →NEWS_SUFFIX_TAKEN(INV-NWS-001). - Правка метаданных. Обновить
Name/Suffix/Title/Subtitle/CategoryId/ProjectRefs, actor-stamp, инкремент доменной версии.Status/PublishedAt/FirstPublishedAt/CurrentVersionNumberне трогаются;PostVersionне добавляется. Если изменилисьName/Title/Subtitle— пересчитать денормализованныйSearchTextв том же изменении (INV-NWS-008); дляPublished-поста — обновить поисковую проекциюnews-posts(search/posts.md). Конкурентная правка →NEWS_CONCURRENCY_CONFLICT(409, retry с перечиткой). Интеграционных событий нет — метаданные без (re)publish не покидаютnews. - Результат.
PostResultиз обновлённого поста →200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (без интеграционных событий; post-published — только на (re)publish) |
| External calls | none (категория/суффикс читаются из своей БД; selections не вызывается — блоки не трогаются) |
| Cache / projections / search | пересчёт денорм. SearchText; для Published-поста — upsert Elasticsearch-проекции news-posts (INV-NWS-008, search/posts.md) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | формат/длины полей, kebab-case суффикса, ProjectIds > 20/дубли | «Проверьте корректность полей» | no retry |
NEWS_POST_NOT_FOUND | 404 | пост не найден или Archived (soft-deleted) | «Пост не найден» | no retry |
NEWS_CATEGORY_NOT_FOUND | 404 | CategoryId не существует | «Категория не найдена» | no retry |
NEWS_CATEGORY_NOT_ALLOWED | 409 | category.Status != Active ∨ AuthorType ∉ AllowedAuthorTypes (INV-NWS-002) | «Категория недоступна для этого типа автора» | no retry |
NEWS_ATTRIBUTION_NOT_ALLOWED | 409 | непустой ProjectRefs при AuthorType != DeveloperManager (INV-NWS-003) | «Привязка к проектам недоступна» | no retry |
NEWS_SUFFIX_TAKEN | 409 | суффикс занят другим неудалённым постом (INV-NWS-001) | «Суффикс уже используется» | no retry |
NEWS_CONCURRENCY_CONFLICT | 409 | конфликт xmin/Version при Update | «Пост изменён параллельно, повторите» | retry (перечитка) |
FORBIDDEN | 403 | нарушение record-level (INV-NWS-007) | «Недостаточно прав на правку поста» | no retry |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — автор поста, менеджер компании-автора в scope
AuthorCompanyId, либо носительnews.posts.manage(INV-NWS-007); - пост существует, неудалён и в редактируемом статусе
Draft/Unpublished/Published; - целевая категория существует,
Activeи допускаетAuthorType(eligibility), новыйSuffixсвободен среди неудалённых (кроме самого поста).
Постусловия (гарантированы после успешной правки):
- обновлены
Name/Suffix/Title/Subtitle/CategoryId/ProjectRefs;Status/PublishedAt/FirstPublishedAt/CurrentVersionNumberне изменены; новыйPostVersionне добавлен (INV-NWS-005 / INV-PV-004 — версия только при (re)publish); - при изменении
Name/Title/Subtitleпересчитан денормализованныйSearchText(INV-NWS-008); доменная версия поста +1; actor-stampUpdatedByUserId/UpdatedAt; - интеграционных Kafka-событий нет — правка метаданных без (re)publish не покидает
news.
Интеграционная последовательность
Всё выполняется синхронно в запросе и фиксируется атомарно (правка полей + пересчёт SearchText). Кросс-сервисных синхронных вызовов нет: категория и уникальность суффикса читаются из своей схемы, selections не вызывается (блоки не трогаются). Что предоставляет наружу: для Published-поста — обновление поисковой проекции news-posts (upsert по изменившемуся SearchText/метаданным, окно свежести — секунды; в этот промежуток лента и чтение поста уже показывают новые метаданные из source of truth, поиск догоняет). Что ожидает: реплика категории/справочника локальна; внешних гарантий не требует. Конкурентность: оптимистическая блокировка версии поста — при устаревшей версии → NEWS_CONCURRENCY_CONFLICT, проигравший перечитывает и повторяет; компенсаций нет — одна строка агрегата.
Acceptance Criteria
- Правка метаданных черновика. Дано: автор, пост
Draft, валидные поля, категорияActiveи eligible,Suffixсвободен. Когда:PUT /api/v1/posts/{id}. Тогда:200, поля обновлены,Status/CurrentVersionNumberнеизменны,PostVersionне добавлен, событий нет. - Правка опубликованного без смены статуса. Дано: пост
Published, изменёнTitle. Когда:PUT. Тогда:200,Status = Published,PostVersionне пишется, поисковая проекцияnews-postsобновлена по новомуSearchText. - Смена категории на недопустимую. Дано:
AuthorType ∉ Category.AllowedAuthorTypesили категория неActive(INV-NWS-002). Когда:PUT. Тогда:409 NEWS_CATEGORY_NOT_ALLOWED. - Недопустимая атрибуция проектов. Дано: непустой
ProjectRefsприAuthorType != DeveloperManager(INV-NWS-003). Когда:PUT. Тогда:409 NEWS_ATTRIBUTION_NOT_ALLOWED. - Занятый суффикс. Дано:
Suffixиспользуется другим неудалённым постом (INV-NWS-001). Когда:PUT. Тогда:409 NEWS_SUFFIX_TAKEN. - Категория не найдена. Дано:
CategoryIdне существует. Когда:PUT. Тогда:404 NEWS_CATEGORY_NOT_FOUND. - Пост в архиве/не найден. Дано: пост
Archivedили отсутствует. Когда:PUT. Тогда:404 NEWS_POST_NOT_FOUND. - Не владелец. Дано: актор не автор, не менеджер в scope и без
news.posts.manage(INV-NWS-007). Когда:PUT. Тогда:403 FORBIDDEN. - Конкурентная правка. Дано: пост изменён параллельно (устаревшая версия). Когда:
PUT. Тогда:409 NEWS_CONCURRENCY_CONFLICT, клиент перечитывает и повторяет. - Невалидный вход. Дано: нарушены длины/паттерн
Suffix,ProjectIds > 20/дубли. Когда:PUT. Тогда:400 VALIDATION_FAILED.
Совместимость и наблюдаемость
Subtitle/ProjectRefs— optional; новые optional-поля запроса/ответа — additive; удаление/ переименование/смена типа — breaking (migration + rollout).PostResult— стабильный контракт; enum-поля (PostStatus/PostAuthorType) сериализуются строкой, unknown значения не вводятся.- Logs:
post-metadata received/updated(PostId,ActorUserId, изменённые поля); metrics:news_post_update_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardnews-overview; runbook — «пост показывает не тот состав» (сверка после правок).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности:
post.md(поля, INV-NWS-001/002/003/005/007/008,PostProjectRefs,PostStatus/PostAuthorType),post-category.md(eligibility INV-CAT-002,AllowedAuthorTypes),post-version.md(INV-PV-004 — правка версию не пишет),post-block.md(SearchText, блоки — иная операция). - Соседние authoring-методы:
POST /api/v1/posts(создание черновика),PUT /api/v1/posts/{id}/blocks(блоки),POST /api/v1/posts/{id}/publish(публикация +PostVersion) — карта в README. - Permission-каталог:
identity/domain/permission.md(news.posts.manage-own,news.posts.manage). - ADR: ADR-0052, ADR-0056.