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:
@@ -7,6 +7,11 @@ import { db } from "../db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { tasksMinimalCache } from "../utils/cache";
|
||||
import { clampTasksLimit, parseTasksCursor, buildTasksNextCursor } from "../utils/task-pagination";
|
||||
import { clampDirectoryRowsLimit } from "../utils/data-table-rows-query";
|
||||
|
||||
// Лимиты выдачи уникальных значений в /column-values (автокомплит фильтров)
|
||||
const COLUMN_VALUES_DEFAULT_LIMIT = 20;
|
||||
const COLUMN_VALUES_MAX_LIMIT = 1000;
|
||||
|
||||
function safeParseOptions(opts: unknown): unknown {
|
||||
if (Array.isArray(opts)) return opts;
|
||||
@@ -478,9 +483,6 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
|
||||
const num = parsed?.value ? String(parsed.value) : str;
|
||||
const entityTitle = parsed?.legalEntityTaskId ? legalEntityMap?.get(Number(parsed.legalEntityTaskId)) : undefined;
|
||||
const result = entityTitle ? `${num} — ${entityTitle}` : num;
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('[getTaskColumnValue] contract-number:', 'fieldId:', fieldId, 'parsed:', JSON.stringify(parsed), 'entityTitle:', entityTitle, 'result:', result);
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return str;
|
||||
@@ -640,17 +642,11 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
|
||||
|
||||
// Загружаем юрлица для полей типа 'contract-number'
|
||||
const contractNumberFields = formFields.filter((f) => f.type === 'contract-number' && (f as any).options?.contractNumber?.legalEntityFormId);
|
||||
console.log('[column-values] contractNumberFields count:', contractNumberFields.length, 'formId:', formId);
|
||||
for (const f of contractNumberFields) {
|
||||
console.log('[column-values] contract-number field:', f.id, f.name, 'options:', JSON.stringify(f.options));
|
||||
}
|
||||
const legalEntityMap = new Map<number, string>();
|
||||
for (const f of contractNumberFields) {
|
||||
const leFormId = (f as any).options.contractNumber.legalEntityFormId as number;
|
||||
console.log('[column-values] loading legal entities for formId:', leFormId, 'org:', req.organizationId);
|
||||
try {
|
||||
const leTasks = await storage.getTasksWithFieldsByForm(leFormId, req.organizationId!);
|
||||
console.log('[column-values] loaded legal entities:', leTasks.length);
|
||||
for (const t of leTasks) {
|
||||
legalEntityMap.set(t.id, t.title);
|
||||
}
|
||||
@@ -658,7 +654,6 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
|
||||
console.error('[column-values] error loading legal entities for formId:', leFormId, err);
|
||||
}
|
||||
}
|
||||
console.log('[column-values] legalEntityMap size:', legalEntityMap.size);
|
||||
|
||||
// Загружаем справочники для полей типа 'table' чтобы резолвить ID в названия
|
||||
const tableRowMap = new Map<number, Map<number, string>>();
|
||||
@@ -721,21 +716,8 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
|
||||
const statusMap = new Map(statuses.map((s: FormStatus) => [s.id, s]));
|
||||
const userMap = new Map(users.map((u: SafeUser) => [u.id, u]));
|
||||
|
||||
// Отладка: логируем select/radio-group/table поля и их options
|
||||
if (column.startsWith('field_')) {
|
||||
const dbgFieldId = parseInt(column.replace('field_', ''));
|
||||
const dbgField = formFields.find(f => f.id === dbgFieldId);
|
||||
if (dbgField && (dbgField.type === 'select' || dbgField.type === 'radio-group' || dbgField.type === 'table')) {
|
||||
console.log('[column-values] field:', dbgField.id, 'type:', dbgField.type, 'options:', JSON.stringify(dbgField.options), 'optionsType:', typeof dbgField.options);
|
||||
// Логируем первую задачу с ненулевым значением
|
||||
const firstTask = taskList.find(t => t.fieldValues && t.fieldValues[dbgField.id] !== undefined && t.fieldValues[dbgField.id] !== null);
|
||||
if (firstTask) {
|
||||
const rawV = firstTask.fieldValues![dbgField.id];
|
||||
const resolved = getTaskColumnValue(firstTask, column, statusMap, userMap, formFields, roleMap, tableRowMap, legalEntityMap, taskTitleMap);
|
||||
console.log('[column-values] firstTaskId:', firstTask.id, 'rawV:', JSON.stringify(rawV), 'rawVType:', typeof rawV, 'resolved:', resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Кап на количество уникальных значений: параметр limit (дефолт 20, максимум 1000)
|
||||
const limit = clampDirectoryRowsLimit(req.query.limit, COLUMN_VALUES_DEFAULT_LIMIT, COLUMN_VALUES_MAX_LIMIT);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const values: string[] = [];
|
||||
@@ -746,7 +728,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
|
||||
if (search && !val.toLowerCase().includes(search)) continue;
|
||||
seen.add(val);
|
||||
values.push(val);
|
||||
if (values.length >= 20) break;
|
||||
if (values.length >= limit) break;
|
||||
}
|
||||
|
||||
res.json({ values });
|
||||
|
||||
Reference in New Issue
Block a user