87 lines
5.5 KiB
Markdown
87 lines
5.5 KiB
Markdown
# Спецификация: `POST /api/v1/sync` (единица 3)
|
||
|
||
> Формат пакета — module_reqs разд. 4 (копия tech-requirements 2.1–2.2).
|
||
|
||
## Запрос
|
||
|
||
JSON-массив записей (пакет ≤ 50):
|
||
|
||
```json
|
||
[
|
||
{ "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" }` |
|
||
|
||
## Логика
|
||
|
||
1. Десериализация и валидация пакета (все записи до начала записи);
|
||
любая ошибка схемы → `400`, в БД ничего не пишется.
|
||
2. Одна транзакция на пакет (🛡 атомарность «все или ничего») —
|
||
`TransactionScope`; Kit.Helpers открывает соединение на каждый вызов,
|
||
Npgsql подключает их к ambient-транзакции. Все ЧТЕНИЯ (проверки 409,
|
||
группы, существование workout'ов) выполняются до открытия
|
||
транзакции — select-функции Kit открывают собственную транзакцию
|
||
(вложенная запрещена); внутри — только записи:
|
||
- записи применяются в порядке массива;
|
||
- `athlete_id` для всех записей — из middleware (🛡, из тела не берётся).
|
||
3. Маппинг (репозитории 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`).
|
||
4. 🛡 Идемпотентность: PK uuid сущностей = `client_guid`; повтор пакета —
|
||
insert-функции возвращают существующие id, дублей нет; ответ — тот же
|
||
`accepted` (для этого `program_version_insert` дополнен
|
||
идемпотентностью по `p_id` — errata m01, 2026-10-18).
|
||
5. Ошибка БД внутри транзакции → 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` |
|
||
|
||
## Пример вызова
|
||
|
||
```bash
|
||
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..."]}
|
||
```
|