Метод PATCH /api/v1/recipes/custom/{id}

Документация API: Точечное обновление свойств авторского рецепта (PATCH) с частичной Pydantic-экстраполяцией

Published

July 2, 2026

1 Функциональное назначение

Метод предназначен для частичного (точечного) изменения структуры кастомного рецепта пользователем в рамках своей группы (home_group_id).

В отличие от тяжелого метода PUT, который требует полной перезаписи объекта, PATCH решает следующие задачи: 1. Экономия сетевого трафика (Low Overheads): Позволяет изменить только отдельные свойства рецепта (например, исправить опечатку в описании или заменить один текстовый шаг приготовления), не гоня по сети массив ингредиентов технологической карты. 2. Асинхронная инвалидация кэша: Автоматически уведомляет ИИ-контур аналитики КБЖУ об изменении метаданных рецепта через шину данных Apache Kafka. 3. Изоляция структуры: Сохраняет неизменными дочерние строки таблицы ингредиентов, если в JSON-пакете не передано поле ingredients, разгружая дисковую подсистему СУБД от лишних операций удаления и записи.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: PATCH
  • Маршрут: /api/v1/recipes/custom/{id}
  • Формат данных: application/json

2.1 Спецификация параметров пути (Path Parameters)

Параметр Тип Обязательный Описание Пример значения
id Integer Да Уникальный ID изменяемого кастомного рецепта в базе группы 412

2.2 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Формат передаваемых данных application/json
Authorization Да Токен авторизации (Access Token) для проверки прав владения кулинарной книгой Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для распределенного логирования req-rec-patch-55bb

2.3 Спецификация тела запроса (Request Body)

Все поля являются опциональными. Валидатор Pydantic на FastAPI модифицирует строго те ключи, которые физически присутствуют в переданном JSON-пакете.

Поле Тип Обязательный Описание Пример значения
title String Нет Новое текстовое название рецепта "Бабушкин яблочный пирог v3"
description String Нет Модифицированные кулинарные заметки к блюду "Проверенный временем рецепт"
cooking_steps Array Нет Обновленный массив текстовых инструкций ["Новый Шаг 1...", "Шаг 2..."]

2.3.1 Пример сырого JSON-запроса (Payload — Сценарий: пользователь исправляет только текстовое описание, не затрагивая состав):

{
  "description": "Проверенный временем рецепт"
}

3 Схема обработки запроса пользователя (Mermaid)

На диаграмме представлена логика частичного обновления (PATCH) кастомного рецепта. Сервис рецептов изолированно фиксирует изменения в PostgreSQL, после чего асинхронно публикует событие в брокер Apache Kafka. Смежные сервисы (ИИ-аналитика и Уведомления) подхватывают этот сигнал, а основной контур Готовки (Cook) при следующем вызове заберет обновленный эталон через gRPC.

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Книги)
    participant APP as Мобильное приложение (Flutter)
    participant RS as Сервис Рецептов (FastAPI)
    participant DB as База Данных (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)
    participant CK as Контур Готовки (Cook)

    User->>APP: Корректирует описание рецепта в интерфейсе
    APP->>RS: HTTP PATCH /api/v1/recipes/custom/412 (JSON с новым описанием)
    activate RS
    
    note over RS: Шаг 3: Декодирование JWT (Извлечение home_group_id)
    
    Note over RS, DB: Старт атомарной SQL-транзакции
    RS->>DB: SELECT * FROM user_custom_recipes WHERE id = 412 FOR UPDATE
    activate DB
    DB-->>RS: Возвращает текущее состояние записи
    deactivate DB
    
    note over RS: Шаг 6: Проверка принадлежности к home_group_id группы
    note over RS: Шаг 7: Pydantic-слияние (Экстраполяция только переданных полей)
    
    RS->>DB: UPDATE user_custom_recipes SET description = \$1 WHERE id = 412
    Note over RS, DB: Коммит транзакции (COMMIT)
    
    RS->>KAFKA: send_to_kafka_async("masterdata.recipes.mutated.v1")
    RS-->>APP: HTTP 200 OK (status: "UPDATED")
    deactivate RS
    
    note over KAFKA: Шаг 11: Асинхронная инвалидация кэша ИИ-аналитики
    
    %% ДЕМОНСТРАЦИЯ ИНТЕГРАЦИОННОЙ СВЯЗИ
    Note over CK, RS: Вызов эталона при запуске готовки пользователем
    CK->>RS: gRPC GetRecipeIngredients(recipe_id=412)
    activate RS
    RS-->>CK: gRPC RecipeIngredientsResponse (Массив свежих данных)
    deactivate RS

4 Расшифровка шагов

  • Шаг 1 (User -> APP): Пользователь в кулинарной книге изменяет одно или несколько текстовых полей рецепта (например, переписывает поле описания "description", оставляя шаги и состав ингредиентов неизменными) и нажимает кнопку «Сохранить изменения».
  • Шаг 2 (APP -> RS): Мобильное приложение Flutter через клиент Dio отправляет запрос HTTP PATCH /api/v1/recipes/custom/{id}. В теле JSON передается исключительно измененный ключ "description". В заголовках прокидывается Access Token авторизации и сквозной X-Request-ID.
  • Шаг 3 (RS -> RS): Микросервис Рецептов (FastAPI) перехватывает запрос, валидирует и декодирует JWT. Из токена извлекается параметр home_group_id для проверки прав доступа к приватной кулинарной книге.
  • Шаг 4 (RS -> DB): Сервис открывает атомарную SQL-транзакцию в PostgreSQL и блокирует целевую строку рецепта: SELECT id, home_group_id, description FROM user_custom_recipes WHERE id = $1 FOR UPDATE.
  • Шаг 5 (DB -> RS): База данных возвращает текущую физическую структуру записи из таблицы кастомных рецептов.
  • Шаг 6 (RS -> RS): Проверка контекста безопасности (Security Scope): Сервис сверяет поле home_group_id из базы данных с home_group_id, зашитым в токене пользователя. Если идентификаторы не совпадают (попытка изменить рецепт чужой семьи), транзакция откатывается (ROLLBACK) с ошибкой HTTP 403 Forbidden.
  • Шаг 7 (RS -> RS): Pydantic-экстраполяция: FastAPI берет структуру из базы данных и накладывает на неё входящий JSON-патч с помощью встроенного механизма частичного обновления response_model.model_validate(existing_db_model, update=patch_data). Изменяются только переданные поля, остальные (включая массив шагов) сохраняют свои исходные значения.
  • Шаг 8 (RS -> DB): Обновленный объект записывается обратно в базу данных: UPDATE user_custom_recipes SET description = $1, updated_at = NOW() WHERE id = $2. Транзакция успешно закрывается командой COMMIT.
  • Шаг 9 (RS -> KAFKA): Сервис Рецептов выступает в роли продюсера (Producer) и отправляет асинхронное системное уведомление в топик Kafka masterdata.recipes.mutated.v1 с типом события "RECIPE_UPDATED". Сообщение содержит recipe_id и home_group_id.
  • Шаг 10 (RS -> APP): Сервер возвращает мобильному приложению статус HTTP 200 OK с подтверждением частичной модификации. Экран во Flutter разблокируется.
  • Шаг 11 (KAFKA -> KAFKA): Асинхронное следствие: шина сообщений Kafka доставляет событие до ИИ-сервиса аналитики КБЖУ. ИИ сбрасывает (инвалидирует) старый кэш структуры рецепта для этой группы и перестраивает NLP-эмбеддинги.
  • Шаг 12 (CK -> RS): Интеграционное взаимодействие (gRPC): Когда пользователь этой группы запускает метод фиксации готовки по рецепту (из микросервиса Cook, Метод 4), транзакционный шлюз готовки обращается к сервису Рецептов через внутренний gRPC-канал: gRPC GetRecipeIngredients(recipe_id=412). Сервис Рецептов мгновенно возвращает актуальный эталон технологической карты по протоколу HTTP/2, изолируя основную базу холодильника от структуры хранения кулинарных книг.

5 Спецификация ответов сервера (Response Body) и ошибок

5.1 1. Успешный ответ (Success Response)

5.1.1 HTTP 200 OK (Ответ на Шаге 10)

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "RECIPE_PARTIALLY_UPDATED",
  "data": {
    "recipe_id": 412,
    "home_group_id": "group-772-alpha",
    "updated_fields": {
      "description": "Проверенный временем рецепт"
    },
    "timestamp": "2026-07-02T01:07:02Z"
  }
}

5.2 2. Спецификация интеграционного gRPC-контракта (Шаг 12)

Описание структуры Protocol Buffers (recipes_integration.proto) для внутреннего синхронного взаимодействия между бэкенд-шлюзом Готовки (Cook) и сервисом Рецептов.

syntax = "proto3";

package masterdata.recipes.v1;

service RecipesIntegrationService {
  // Вызывается контуром Cook для получения эталона перед списанием продуктов
  rpc GetRecipeIngredients (RecipeIngredientsRequest) returns (RecipeIngredientsResponse);
}

message RecipeIngredientsRequest {
  int32 recipe_id = 1;
  string home_group_id = 2;
}

message RecipeIngredientItem {
  string ingredient_name = 1; // Каноническое имя продукта
  double required_amount = 2;  // Вес или штуки
  string unit = 3;             // Единица измерения ("кг", "шт")
}

message RecipeIngredientsResponse {
  int32 recipe_id = 1;
  string title = 2;
  repeated RecipeIngredientItem ingredients = 3; // Массив для каскадного автосписания
}

5.3 3. Вилки исключений и обработка ошибок

5.3.1 Ошибка: Попытка изменения глобального системного рецепта (HTTP 403 Forbidden)

Выбрасывается на Шаге 6, если переданный id принадлежит глобальной неизменяемой базе рецептов экосистемы (master_recipes), которые защищены от записи.

{
  "error_code": "ERR-SYSTEM-RECIPE-IMMUTABLE",
  "message": "Операция отклонена: базовые рецепты экосистемы защищены от записи и доступны только для чтения.",
  "details": {
    "requested_id": 12,
    "table_scope": "master_recipes"
  }
}

5.3.2 Ошибка: Нарушение прав доступа к чужой кулинарной книге (HTTP 403 Forbidden — Шаг 6)

Выбрасывается, если home_group_id текущей сессии пользователя не совпадает с home_group_id владельца рецепта в базе данных.

{
  "error_code": "ERR-RECIPE-ACCESS-DENIED",
  "message": "Доступ заблокирован: у вас нет прав на редактирование рецептов чужой группы.",
  "details": {
    "reason": "Security scope verification failed for target recipe entity."
  }
}