Шаги 0.10 и 0.9 плана production-готовности: - дефолт limit 50, кап 200; списковые ответы без description - курсорная пагинация (updated_at, id) + nextCursor - since-фильтр дельта-синка перенесён в SQL (updated_at >= since) - embedding pre-count через COUNT(*) вместо выгрузки миллиона строк - 11 новых юнит-тестов (56/56 зелёные) Инвариант поиска сохранён: search-ветка /api/tasks?search= не изменена
45 lines
2.0 KiB
TypeScript
45 lines
2.0 KiB
TypeScript
/**
|
||
* Хелперы пагинации спискового endpoint'а GET /api/tasks.
|
||
* Дефолтный лимит 50, жёсткий максимум 200 — защита от выгрузки
|
||
* всей таблицы задач одним запросом без параметров.
|
||
*/
|
||
|
||
export const TASKS_LIST_DEFAULT_LIMIT = 50;
|
||
export const TASKS_LIST_MAX_LIMIT = 200;
|
||
|
||
/** Курсорная пагинация по (updatedAt, id): ключевая пара последней строки выдачи */
|
||
export interface TasksCursor {
|
||
updatedAt: Date;
|
||
id: number;
|
||
}
|
||
|
||
/** Приводит запрошенный лимит к диапазону 1..200; без значения — дефолт 50 */
|
||
export function clampTasksLimit(requested?: number | null): number {
|
||
if (requested == null || Number.isNaN(requested) || requested <= 0) {
|
||
return TASKS_LIST_DEFAULT_LIMIT;
|
||
}
|
||
return Math.min(Math.floor(requested), TASKS_LIST_MAX_LIMIT);
|
||
}
|
||
|
||
/**
|
||
* Парсит курсор формата `<updatedAt ISO>_<id>`.
|
||
* Невалидный курсор возвращает null — запрос обрабатывается как первая страница.
|
||
*/
|
||
export function parseTasksCursor(raw?: string | null): TasksCursor | null {
|
||
if (!raw) return null;
|
||
const idx = raw.lastIndexOf('_');
|
||
if (idx <= 0 || idx === raw.length - 1) return null;
|
||
const ts = new Date(raw.slice(0, idx)).getTime();
|
||
const id = Number(raw.slice(idx + 1));
|
||
if (Number.isNaN(ts) || !Number.isInteger(id) || id <= 0) return null;
|
||
return { updatedAt: new Date(ts), id };
|
||
}
|
||
|
||
/** Собирает nextCursor из последней строки выдачи; null, если у строки нет updatedAt */
|
||
export function buildTasksNextCursor(task: { id: number; updatedAt: Date | string | null }): string | null {
|
||
if (!task.updatedAt) return null;
|
||
const ts = new Date(task.updatedAt).getTime();
|
||
if (Number.isNaN(ts)) return null;
|
||
return `${new Date(ts).toISOString()}_${task.id}`;
|
||
}
|