Цель: для больших списков заменить offset-пагинацию на keyset (cursor) — стабильный порядок и отсутствие деградации на глубоких страницах.
Зависимость: #4, #8.
Что сделать
- Добавь keyset-вариант для
GET /api/items и истории движений (GET /api/movements/{itemId}/history). Offset (page/size) для совместимости можно оставить — keyset включается параметром cursor.
- Запрос вместо
OFFSET: WHERE (sort_field, id) < (:lastSort, :lastId) ORDER BY sort_field, id LIMIT :size. Обязателен устойчивый tie-break по id — иначе строки с равным ключом сортировки «прыгают».
- Курсор в ответе — непрозрачная строка (Base64 от
lastSort+lastId), не сырой offset:
{ "content": [...], "nextCursor": "eyJpZCI6NDIs..." , "hasNext": true }
- Трудные места: кодирование/декодирование и валидация курсора (битый курсор → 400, а не 500); согласованность поля сортировки в курсоре и в
ORDER BY; что делать при смене направления/поля сортировки между запросами.
Acceptance criteria
Цель: для больших списков заменить offset-пагинацию на keyset (cursor) — стабильный порядок и отсутствие деградации на глубоких страницах.
Зависимость: #4, #8.
Что сделать
GET /api/itemsи истории движений (GET /api/movements/{itemId}/history). Offset (page/size) для совместимости можно оставить — keyset включается параметромcursor.OFFSET:WHERE (sort_field, id) < (:lastSort, :lastId) ORDER BY sort_field, id LIMIT :size. Обязателен устойчивый tie-break поid— иначе строки с равным ключом сортировки «прыгают».lastSort+lastId), не сырой offset:{ "content": [...], "nextCursor": "eyJpZCI6NDIs..." , "hasNext": true }ORDER BY; что делать при смене направления/поля сортировки между запросами.Acceptance criteria
GET /api/items?cursor=...отдаёт страницу +nextCursor