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

@@ -42,7 +42,7 @@ export interface IStorage {
getUserWithOrganization(id: number): Promise<UserWithOrganization | undefined>;
getUserByEmail(email: string, organizationId?: number): Promise<User | undefined>;
getUserByEmailAndSlug(email: string, slug: string): Promise<UserWithOrganization | undefined>;
getUsersByOrganization(organizationId: number): Promise<SafeUser[]>;
getUsersByOrganization(organizationId: number, options?: { since?: Date }): Promise<SafeUser[]>;
listUsers(organizationId: number, options?: import('./storage/users.storage').ListUsersOptions): Promise<import('./storage/users.storage').ListUsersResult>;
getLinkedTasksForUser(userId: number, organizationId: number): Promise<
Array<{
@@ -182,7 +182,7 @@ export interface IStorage {
} | null>;
getTasksByForm(formId: number, organizationId: number): Promise<Task[]>;
getTasksWithFieldsByForm(formId: number, organizationId: number): Promise<Task[]>;
getTasksWithFieldsByFormOptimized(formId: number, organizationId: number, options?: { limit?: number; offset?: number; assignedTo?: number }): Promise<{ tasks: Task[]; total: number }>;
getTasksWithFieldsByFormOptimized(formId: number, organizationId: number, options?: { limit?: number; offset?: number; assignedTo?: number; since?: Date }): Promise<{ tasks: Task[]; total: number }>;
getTasksByOrganization(organizationId: number, options?: {
limit?: number;
offset?: number;
@@ -191,7 +191,10 @@ export interface IStorage {
statusId?: number;
assignedTo?: number;
minimal?: boolean;
excludeDescription?: boolean;
cursor?: import('./utils/task-pagination').TasksCursor | null;
}): Promise<Task[] | Partial<Task>[]>;
getTasksCountByOrganization(organizationId: number): Promise<number>;
createTask(insertTask: InsertTask): Promise<Task>;
updateTask(id: number, organizationId: number, updates: Partial<Task>): Promise<Task>;
updateTaskIfNotModified(id: number, organizationId: number, updates: Partial<Task>, requiredUpdatedAt: Date): Promise<Task | null>;