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

PUT /api/v1/selections/{id} — Изменить название/описание подборки

Правка метаданных подборки владельцем: Title и Description. Состав меняется отдельной операцией set-selection-items.md; PublicCode неизменяем (INV-SEL-005).

Назначение

Владелец переименовывает подборку или правит описание. Уже снятые для постов snapshot-ы не меняются (INV-SEL-001 — их Content.Title заморожен). Дом полей — ../domain/selection.md.

Metadata

ПолеЗначение
Сервис/контекстselections
API areapublic
Feature groupselections
Статусdraft
Документdocs/06-services/selections/api/public/update-selection.md

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

ПроверкаЗначение
Actor typeUser (JWT)
Auth policy / Permissionsauthenticated user; отдельных permissions нет (self-scope)
Scope (RBAC)self
Record-levelOwnerUserId == actor.UserId в предикате загрузки (INV-SEL-002); чужая/несуществующая → 404 без раскрытия существования
Tenant isolationне применяется — вне tenant (ADR-0052 п.2)
Auditupdate: UpdatedAt + SelectionUpdatedKafkaEventuser-activity

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/v1/selections/{id:guid}
Success status200
Idempotency headerне применяется — PUT идемпотентен семантически (полная замена метаданных)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
IdrouteGuidДа!= Guid.Emptyrecord-level фильтр по владельцу
UserIdidentity claims (sub)GuidДаRequired
TitlebodystringДа1..256 после trim
Descriptionbodystring?Нет≤ 2000 после trim; пустая строка → null (очистка описания)

PublicCode, Source, OwnerUserId в запросе не принимаются — неизменяемые поля.

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

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаSelection.IdПодборка.
TitlestringДаSelection.Title после правкиАктуальное название.
Descriptionstring?НетSelection.DescriptionАктуальное описание.
UpdatedAtDateTimeOffsetДаSelection.UpdatedAtШтамп правки.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь; править можно только собственную подборку (по владельцу-актору).
  2. Проверка входа и предусловий. Id непустой; Title 1..256 после trim; Description ≤ 2000 — иначе VALIDATION_ERROR. Подборка должна существовать, принадлежать актору (INV-SEL-002) и быть не удалённой (INV-SEL-009); иначе единый SELECTIONS_NOT_FOUND (существование чужой не раскрывается).
  3. Что читаем. Подборка по id и владельцу-актору, не удалённая (метаданные корня, состав не нужен).
  4. Бизнес-правила и переходы. Переход Active → Active (переименование): присваиваются Title, Description (после trim, пустое описание очищается), UpdatedAt. При полном совпадении значений — no-op (событие не поднимается, ответ 200 с текущим состоянием); при изменении — фиксируется факт правки метаданных.
  5. Что меняется. Обновляется корень подборки (элементы не затрагиваются) в одном согласованном изменении. Конфликт с параллельной правкой (другая вкладка) → SELECTIONS_CONCURRENCY_CONFLICT (409), клиент перечитывает и повторяет.
  6. Результат. Модель UpdateSelectionResult (Id, Title, Description, UpdatedAt) → 200.

Публикуемое интеграционное событие — в разделе «Побочные эффекты».

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияSelectionUpdatedKafkaEvent / pin.selections.selection-updated.v1 / key = SelectionId (только при фактическом изменении)
Проекции / поискsnapshot-ы постов сознательно не трогаются (INV-SEL-001)

Интеграционные последствия (асинхронно, через outbox)

Событие уходит через outbox после коммита правки (at-least-once, key = SelectionId — относительный порядок правок одной подборки сохраняется). Цепочка:

  1. selection-updateduser-activity: фиксирует ActionEvent «изменил подборку» в истории действий владельца; если владелец-агент, событие питает карточку лида CRM (ADR-0054).
  2. Окно eventual-consistency: между 200 и записью в истории действий — секунды (лаг outbox + консюмер). Новое название владелец и открывшие ссылку видят сразу (read-your-writes из основной БД); лента действий догоняет.
  3. No-op (значения не изменились) события не порождает — история действий не засоряется пустыми правками; snapshot-ы постов от переименования никогда не меняются (INV-SEL-001 — Content.Title заморожен на момент снятия).
  4. Потребитель идемпотентен по SelectionId (+ порядковый штамп события) — повторная доставка не дублирует запись в истории.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_ERROR400Title/Description вне ограничений«Некорректные данные подборки»no retry
SELECTIONS_NOT_FOUND404Нет записи / чужая (INV-SEL-002) / удалена (INV-SEL-009)«Подборка не найдена»no retry
SELECTIONS_CONCURRENCY_CONFLICT409Конфликт оптимистической блокировки (параллельная правка)«Подборка изменена в другой вкладке — обновите страницу»retry после перечитывания
UNAUTHORIZED401Нет/просрочен access-токенretry после refresh

Предусловия и постусловия

  • Предусловия: актор авторизован (валидный JWT); подборка существует, принадлежит актору (OwnerUserId == actor.UserId, INV-SEL-002) и не удалена (DeletedAt == null, INV-SEL-009).
  • Постусловия при 200 с изменением: Title/Description подборки равны новым значениям (после trim, пустое описание → null); UpdatedAt продвинут; PublicCode, Source, OwnerUserId, состав элементов и все ранее снятые snapshot-ы не изменились (INV-SEL-001/005); в outbox поставлено ровно одно selection-updated (key = SelectionId).
  • Постусловия при 200-no-op (значения совпали): состояние подборки не изменилось (UpdatedAt не двигается), событий нет.
  • Постусловия при отказе (400/401/404/409): подборка не изменена, событий нет.

Acceptance Criteria

  • Given подборка владельца с Title = "Черновик", When PUT с Title = "Для Ивановых", Then 200, ответ содержит новое название и продвинутый UpdatedAt; поставлено selection-updated (key = SelectionId).
  • Given та же подборка, When PUT с Description = "" (после trim пусто), Then 200, Description очищено в null.
  • Given подборка с текущими Title/Description, When PUT с теми же значениями, Then 200-no-op: UpdatedAt не меняется, событие не публикуется.
  • Given Title из одних пробелов (после trim пусто), When PUT, Then 400 VALIDATION_ERROR, подборка не изменена.
  • Given чужая или несуществующая подборка, When PUT, Then 404 SELECTIONS_NOT_FOUND (существование чужой не раскрывается, INV-SEL-002).
  • Given soft-deleted подборка, When PUT, Then 404 SELECTIONS_NOT_FOUND (INV-SEL-009).
  • Given две параллельные правки из разных вкладок, When обе доходят, Then одна фиксируется, вторая → 409 SELECTIONS_CONCURRENCY_CONFLICT; проигравший перечитывает актуальное состояние и повторяет.
  • Given подборка с уже снятым snapshot-ом поста, When владелец переименовал подборку, Then Content.Title snapshot-а остаётся прежним (INV-SEL-001) — пост показывает название на момент публикации.
  • Given отсутствующий/просроченный токен, When PUT, Then 401 UNAUTHORIZED.

Совместимость и наблюдаемость

  • Новые optional поля — additive; прочее — breaking.
  • Logs: UserId, SelectionId, результат (без Title/Description); metrics: selections_updated_total{kind="metadata"}; traces: span UpdateSelectionCommand. Dashboard — selections-overview.

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

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

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