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

Создание, PATCH и удаление ресурсов

Изменяющая операция переносится целиком: входной контракт, валидация, бизнес-правила, SQL, события, файлы и вызовы других сервисов. Handler собирает вход и ответ, action управляет сценарием и возвращает ошибку. Границы пакетов описаны в структуре сервиса.

1. Описать операцию до реализации​

Найдите OpenAPI, route, request, action, модели, mapper, фабрики и тесты. При портировании дополнительно изучите Laravel FormRequest, casts, observers и транзакции — поведение может находиться за пределами controller.

РешениеЧто зафиксировать
КонтрактMethod/URI, media type, required/optional/nullable, статусы и ошибки
ИзменяемостьКакие поля можно менять, какие immutable и какие вычисляются сервером
ДоступВладелец/родитель, роли и состояние, в котором операция разрешена
ЭффектыТаблицы, timestamps, dirty, события, внешние вызовы, файлы и кэш
ПовторОтсутствующая запись, повторный запрос, те же значения и no-op
ОтказЧто остаётся после ошибки каждого шага и как восстанавливается сценарий

Изменяйте исходный OpenAPI и запускайте elc ensi-gog server; поля generated DTO и маршруты не исправляют вручную. После изменения схемы отдельно сверяйте Rules() и фабрики: генератор сохраняет их доработки. Подробнее — ensi-gog.

2. Сохранить тонкий handler​

Пример PATCH страниц CMS:

func (c *PagesController) Patch(ctx http.Context) http.Response {
var req dto.PatchPageRequest
if failResponse := commonhttp.Validate(ctx, &req); failResponse != nil {
return failResponse
}

model, err := actions.NewPatchPageAction().Execute(
ctx.Context(),
uint(ctx.Request().RouteInt("id")),
req,
ctx.Request().All(),
)
if err != nil {
return commonhttp.HandleError(ctx, err)
}

return commonhttp.Data(ctx, mappers.PageFromModel(*model, search.Params{}))
}

Здесь DTO содержит проверенные значения, а карта полей используется для определения присутствия ключа. В action передаётся context.Context. Числовой id предварительно проверяет общий route middleware; для другого имени/типа path-параметра нужно предусмотреть его фактическую проверку.

При create возвращайте DTO с согласованным статусом, при delete — предусмотренный контрактом ответ. HandleError обрабатывает ORM not found; прикладные отказы задаются типами из app/errors. Полная таблица — в HTTP-ответах и ошибках.

3. Различать состояния поля PATCH​

ВходТипичное решение, которое нужно подтвердить контрактом
Поля нетСохранить прежнее значение
Явный nullОчистить nullable-поле либо отклонить запрещённый null
false, 0, "", []Применить или отклонить именно переданное значение
Значение совпадает с сохранённымСоблюсти правила no-op, timestamps и событий

Указатель *T после обычного JSON-decoding часто одинаково представляет отсутствие поля и null. Нужен совместимый с генератором presence-механизм: отдельный тип, decoder либо карта присутствия после валидации. У CMS она передаётся аргументом fields, у OMS есть отдельный PatchOrderForm.Raw.

Карта присутствия не должна превращаться в mass assignment: присваивайте разрешённые типизированные значения DTO. Пример фрагмента внутри PatchPageAction.Execute, возвращающего (*db.Page, error):

if _, present := fields["is_active"]; present {
if request.IsActive == nil {
return nil, apperrors.NewInvalidArgumentError("is_active must not be null")
}
page.IsActive = *request.IsActive
}

if _, present := fields["active_from"]; present {
page.ActiveFrom = carbonTime(request.ActiveFrom)
}

apperrors — goravel/app/errors; carbonTime — локальное преобразование в actions CMS, которое возвращает nil для отсутствующего значения времени. В этом примере is_active допускает false, но не null, а active_from допускает очистку. Если ключ отсутствует, соответствующее присваивание не выполняется.

Это схема присваиваний, а не весь action: после них проверяются согласованность дат, уникальность и другие инварианты, затем модель сохраняется. После сохранения перечитайте значения, если ответ или событие должны учитывать defaults и timestamps БД.

Вложенная валидация​

Для объектов и массивов перенесите Laravel dot-path rules: контейнер, вложенные поля, элементы и их required/nullable-семантику. Например, OMS отдельно проверяет delivery_address и delivery_address.country_code, address_string и другие поля.

Не извлекайте бизнес-значения через невалидированные InputMap/type assertion. Проверьте неправильный тип контейнера, неправильный вложенный тип, отсутствие обязательного поля и допустимый null. Для PUT и PATCH могут действовать разные требования.

Запись нулевых значений​

У ORM-обновления struct могут пропускаться zero values. Выбирайте способ записи, который действительно сохраняет false, 0 и SQL NULL, и подтверждайте это чтением новой модели из БД. Успешный JSON-ответ из объекта в памяти этого не доказывает.

4. Проверить бизнес-правила и конкуренцию​

Загрузите запись по контрактному ключу и проверьте владельца/родителя. Для статуса нужен допустимый переход и связанные изменения: даты, вычисляемые поля и эффекты. В OMS такой сценарий реализован в PatchOrderAction и helpers переходов.

Для before/after-сравнения учитывайте, что копирование struct сохраняет общие maps, slices и указатели. Исходный снимок не должен измениться вместе с новым состоянием. Dirty fields формируют по реальным изменениям, включая вычисляемые поля; no-op не должен случайно вызывать повторный эффект.

Проверка уникальности чтением до записи полезна для доменной ошибки, но не защищает от параллельной вставки. Инварианты обеспечиваются ограничениями БД, блокировкой или условным обновлением согласно сценарию. Для конкурентного изменения состояния проверяйте состояние под нужной защитой, а не только до начала транзакции.

5. Передавать одну SQL-транзакцию​

Связанные изменения должны использовать один orm.Query. Схема двух записей внутри action:

err := facades.Orm().WithContext(ctx).Transaction(func(tx orm.Query) error {
if err := tx.Save(&model); err != nil {
return err
}
return tx.Create(&link)
})
if err != nil {
return err
}

model и link здесь — подготовленные сущности сценария; фрагмент показывает управление SQL, а не сигнатуру любого action. Вложенные действия должны принимать tx. Получение default ORM query внутри callback выводит часть изменений из транзакции.

SQL и внешние эффекты​

ЭффектГраница отказа
Kafka внутри SQL-транзакцииОшибка отправки может отменить SQL, но уже опубликованное сообщение не откатывается
Kafka после commitSQL уже сохранён; ошибка публикации требует предусмотренного восстановления
Существующий outboxНамерение записывается тем же tx, доставка проверяется вместе с relay и повтором
Внешний HTTP или storageSQL rollback не отменяет действие внешней системы

Сохраняйте архитектуру и момент публикации исходного сценария. Eloquent after-commit observer нельзя автоматически заменить отправкой до commit. FK cascade не воспроизводит observer дочерней модели: иногда нужно отдельно сохранить события удаления связей или удалить файлы.

Обязательный эффект не превращается в успех через логирование ошибки. Для best-effort эффекта явно определите дальнейшую очистку и наблюдаемость. Детали — в Kafka, HTTP-клиентах и работе с файлами.

6. Реализовать удаление и массовые операции​

Сначала определите поведение для отсутствующего ID, повторного удаления, дубликатов, пустого списка и частично найденного набора. Идемпотентный delete и delete с 404 — разные контракты; при портировании следуйте согласованному поведению операции.

Перед разрушительными эффектами загрузите и проверьте весь набор: принадлежность каждой записи, допустимое состояние и полноту, если она обязательна. Присутствие refundID/orderID в URI само по себе не ограничивает SQL-запрос. Проверка первого файла не доказывает ownership остальных.

Для изменения связей различайте локальный ID и ID внешнего сервиса, сверяйте FK и не считайте отсутствие upstream-ошибки подтверждением существования всех переданных сущностей. Проверяйте возвращённые идентификаторы и pagination upstream.

7. Проверить наблюдаемый результат​

Тест вызывает настоящий URI через общий HTTP wrapper, затем перечитывает БД и проверяет эффекты. Для ошибок входа при необходимости отключают только request validation, сохраняя проверку ответа.

SQL-фикстуры готовьте через s.CreateQuietly(...), если их model events не относятся к проверяемой операции. Публикации самой mutation проверяйте через s.AssertKafkaMessages(topicKeys...) и assertions payload/key/headers; для ветки без публикаций — s.AssertKafkaMessages(). Общий teardown отклоняет непроверенные сообщения. При собственном TearDownTest сохраните вызов базового метода, а в ручном suite зарегистрируйте его после setup.

ГруппаОбязательные для применимого сценария случаи
УспехDTO, созданные/изменённые строки, timestamps и связанные данные
PATCHMissing, очистка заполненного nullable, true→false, ненулевое→0, пустые значения и no-op
ОшибкиНевалидный вход, missing record, чужой владелец, конфликт и запрещённый переход
ТранзакцияОтказ после части SQL-работы и отсутствие частичных SQL-изменений
ЭффектыТочные payload/calls/files и их отсутствие при отказе; ошибка эффекта и повтор
ИзоляцияПосторонняя запись не изменена, весь массовый набор проверен

Схема ответа по OpenAPI и HTTP 200 не доказывают правильную запись поля. Проверка null → null не доказывает очистку ранее заполненного значения. Для no-op сверяйте фактические требования к timestamp и публикации, а не вводите общее правило для всех ресурсов.

Используйте implement-api-mutation и подходящие скиллы границ из .agents/skills. Настройка harness — в автотестах.

Исходные примеры​

Пример проверки ресурса CMS:

elc go test ./app/modules/contents/tests/pages --env=.env.testing -count=1
elc mise run check:generated
elc go build -o build/app