perf(tasks): лимиты/пагинация /api/tasks и дельта-синк в SQL

Шаги 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= не изменена
This commit is contained in:
2026-09-07 22:01:25 +03:00
parent 0a60c6abf1
commit a27766a38b
11 changed files with 275 additions and 40 deletions

View File

@@ -258,7 +258,6 @@ export function registerSyncRoutes(app: import("express").Express): void {
const changedTasks: any[] = [];
const changedFieldValues: any[] = [];
const deletedTaskIds: number[] = [];
const changedUsers: any[] = [];
for (const formId of accessibleFormIds) {
const form = await storage.getForm(formId, organizationId);
@@ -274,13 +273,13 @@ export function registerSyncRoutes(app: import("express").Express): void {
// Honor the form's offline-cache strategy for delta sync as well.
const offlineCache = (form as any)?.offlineCache as { enabled?: boolean; strategy?: string; maxTasks?: number } | null | undefined;
const taskOptions: { assignedTo?: number } = {};
const taskOptions: { assignedTo?: number; since?: Date } = { since };
if (offlineCache?.strategy === 'assigned') {
taskOptions.assignedTo = userId;
}
// Фильтр по updated_at — на уровне SQL (раньше грузились ВСЕ задачи формы + JS-фильтр)
const { tasks: formTasks } = await storage.getTasksWithFieldsByFormOptimized(formId, organizationId, taskOptions);
const recentTasks = (formTasks as any[]).filter(t => !t.updatedAt || new Date(t.updatedAt) >= since);
for (const task of recentTasks) {
for (const task of formTasks as any[]) {
const fieldValues = task.fieldValues || {};
delete task.fieldValues;
changedTasks.push(task);
@@ -292,12 +291,8 @@ export function registerSyncRoutes(app: import("express").Express): void {
// TODO: deleted tasks tracking через task_audit_log
}
const allUsers = await storage.getUsersByOrganization(organizationId);
for (const user of allUsers) {
if (!user.updatedAt || new Date(user.updatedAt) >= since) {
changedUsers.push(user);
}
}
// Фильтр по updated_at — на уровне SQL (раньше: полная выгрузка + JS-фильтр)
const changedUsers = await storage.getUsersByOrganization(organizationId, { since });
res.json({
success: true,