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

@@ -1,6 +1,6 @@
import { users, organizations, userSessions, invitations, userCustomFields, userCustomValues, roleMembers, forms, formFields, formStatuses, tasks, taskFieldValues, userOfflineSubscriptions, safeUserColumns, type User, type SafeUser, type Organization, type UserSession, type Invitation, type InsertUser, type InsertOrganization, type InsertUserSession, type InsertInvitation, type UserWithOrganization, type UserCustomField, type UserCustomValue, type CreateUserCustomField, type UpdateUserCustomField, type Task, type FormField, type FormStatus, type UserOfflineSubscription, type InsertUserOfflineSubscription } from "@shared/schema";
import { db } from "../db";
import { eq, and, desc, asc, sql, ilike, or, gte, lte, inArray } from "drizzle-orm";
import { eq, and, desc, asc, sql, ilike, or, gte, lte, inArray, isNull } from "drizzle-orm";
import crypto from "crypto";
import { userStatusesStorage } from "./user-statuses.storage";
@@ -90,12 +90,21 @@ export class UsersStorage {
};
}
// Массовая выдача пользователей клиенту — только безопасные колонки (без хэша пароля и токенов)
async getUsersByOrganization(organizationId: number): Promise<SafeUser[]> {
// Массовая выдача пользователей клиенту — только безопасные колонки (без хэша пароля и токенов).
// options.since — дельта-синк: фильтр по updated_at на уровне SQL
// (NULL updated_at включаем для паритета со старым JS-фильтром).
async getUsersByOrganization(organizationId: number, options?: { since?: Date }): Promise<SafeUser[]> {
const conditions = [eq(users.organizationId, organizationId)];
if (options?.since) {
conditions.push(or(
gte(users.updatedAt, options.since),
isNull(users.updatedAt)
)!);
}
return await db
.select(safeUserColumns)
.from(users)
.where(eq(users.organizationId, organizationId))
.where(and(...conditions))
.orderBy(desc(users.createdAt));
}