perf(directories): SQL-фильтрация/пагинация справочников, N+1, лимиты column-values

Шаги 1.1 и 1.2 плана production-готовности:
- getDataTableRowsPaged: фильтры/поиск/сортировка/пагинация в SQL (values->>N)
- getDataTablesWithAccess и list_directories без N+1
- /column-values: limit (дефолт 20, кап 1000), убраны debug-логи с горячего пути
- tree-режим и legacy-путь TableEditor сохранены 1:1
- 10 новых тестов (88/88)
This commit is contained in:
2026-09-07 23:09:15 +03:00
parent 701af06290
commit 9528a8123a
8 changed files with 348 additions and 99 deletions

View File

@@ -0,0 +1,39 @@
// Хелперы для SQL-пагинации строк справочников (data_table_rows).
// Чистые функции без доступа к БД — покрыты юнит-тестами.
/** Дефолтный лимит строк на страницу */
export const DIRECTORY_ROWS_DEFAULT_LIMIT = 100;
/** Максимальный лимит строк на страницу */
export const DIRECTORY_ROWS_MAX_LIMIT = 1000;
/**
* Экранирует спецсимволы LIKE/ILIKE (\, %, _) в пользовательской подстроке,
* чтобы поиск работал как «содержит подстроку», а не как паттерн.
* Используется вместе с `ESCAPE '\'` в SQL.
*/
export function escapeIlikePattern(value: string): string {
return value.replace(/[\\%_]/g, (ch) => '\\' + ch);
}
/**
* Приводит лимит к допустимому диапазону.
* Невалидные/неположительные значения → defaultLimit, сверху капается maxLimit.
*/
export function clampDirectoryRowsLimit(
raw: unknown,
defaultLimit: number = DIRECTORY_ROWS_DEFAULT_LIMIT,
maxLimit: number = DIRECTORY_ROWS_MAX_LIMIT,
): number {
const n = typeof raw === 'number' ? raw : parseInt(String(raw ?? ''), 10);
if (!Number.isFinite(n) || n <= 0) return defaultLimit;
return Math.min(Math.floor(n), maxLimit);
}
/**
* Приводит offset к неотрицательному целому. Невалидное → 0.
*/
export function clampDirectoryRowsOffset(raw: unknown): number {
const n = typeof raw === 'number' ? raw : parseInt(String(raw ?? ''), 10);
if (!Number.isFinite(n) || n < 0) return 0;
return Math.floor(n);
}