5.5 KiB
5.5 KiB
Спецификация: POST /api/v1/sync (единица 3)
Формат пакета — module_reqs разд. 4 (копия tech-requirements 2.1–2.2).
Запрос
JSON-массив записей (пакет ≤ 50):
[
{ "client_guid": "uuid-v4",
"entity": "workout | workout_set | exercise | program_version | profile | daily_log | break",
"op": "upsert",
"data": { } }
]
Ответы
| Код | Когда | Тело |
|---|---|---|
| 200 | все записи пакета записаны | { "accepted": ["guid1", ...] } — GUID пакета в исходном порядке |
| 400 | пакет > 50; неизвестная entity; op ≠ upsert; невалидный GUID; нарушение схемы/диапазонов; break c source = "auto"; невалидный JSON |
{ "error": "<текст>" } |
| 409 | конфликт версии программы (UNIQUE(program_id, version)) |
{ "error": "program_version_conflict" } |
| 401 | нет/чужой/заблокирован ключ (middleware) | { "error": "unauthorized" } |
Логика
- Десериализация и валидация пакета (все записи до начала записи);
любая ошибка схемы →
400, в БД ничего не пишется. - Одна транзакция на пакет (🛡 атомарность «все или ничего») —
TransactionScope; Kit.Helpers открывает соединение на каждый вызов, Npgsql подключает их к ambient-транзакции. Все ЧТЕНИЯ (проверки 409, группы, существование workout'ов) выполняются до открытия транзакции — select-функции Kit открывают собственную транзакцию (вложенная запрещена); внутри — только записи:- записи применяются в порядке массива;
athlete_idдля всех записей — из middleware (🛡, из тела не берётся).
- Маппинг (репозитории m01):
workout→WorkoutRepository.Insert(идемпотентен по uuid; если вdataестьended_at/status/avg_hrсверх вставки — следомUpdateдля этих полей);workout_set→WorkoutSetRepository.Insert(идемпотентен);workout_idдолжен принадлежать атлету — иначе исключение из функции БД → пакет отклоняется (400, текст ошибки функции);exercise→ExerciseRepository.Insert(идемпотентен;group→group_idпо коду черезExerciseGroupRepository);program_version→ при отсутствии программы атлета сначалаProgramRepository.Insert(title= «Программа»), затемProgramVersionRepository.Insert(body— как есть); 409-проверка — в пре-проходе (п. 2):based_on_versionиз записи ≠ актуальная максимальная версия →409(телефон перечитывает программу); UNIQUE-конфликт вprogram_version_insert— тоже409;profile→AthleteProfileRepository.Upsert;daily_log→DailyLogRepository.Upsert(поUNIQUE(athlete_id, date));break→BreakRepository.Insert(source— толькоmanual).
- 🛡 Идемпотентность: PK uuid сущностей =
client_guid; повтор пакета — insert-функции возвращают существующие id, дублей нет; ответ — тот жеaccepted(для этогоprogram_version_insertдополнен идемпотентностью поp_id— errata m01, 2026-10-18). - Ошибка БД внутри транзакции → rollback →
400(или409для конфликта версии программы), текст ошибки — в лог.
Тесты
| # | Проверка | Ожидание |
|---|---|---|
| 1 | Валидный пакет (по одной записи каждой сущности) | 200, accepted = все GUID, строки в БД |
| 2 | Повтор того же пакета | 200, тот же accepted, дублей в БД нет |
| 3 | Пакет из 51 записи | 400 |
| 4 | Неизвестная entity |
400 |
| 5 | break с source = "auto" |
400 |
| 6 | rpe = 11 |
400, в БД ничего из пакета не записано |
| 7 | Вторая версия программы с тем же version (перезапись uuid) |
409 |
| 8 | workout_set с чужим workout_id (другой атлет) |
400, пакет откатан |
| 9 | Пакет из 50 записей | 200 |
| 10 | program_version при отсутствии программы атлета |
200; созданы program и program_version |
Пример вызова
curl -X POST https://host/api/v1/sync -H "X-Api-Key: ivan-key-1" -H "Content-Type: application/json" \
-d '[{"client_guid":"0b8f...","entity":"daily_log","op":"upsert",
"data":{"date":"2026-10-17","bp_sys":128,"bp_dias":82,"weight_kg":84.5,"hr_resting":58,"wellbeing":4}}]'
# → {"accepted":["0b8f..."]}