perf(access): TTL-кэш getAccessibleTaskIds с точечной инвалидацией

Шаг 0.12 плана production-готовности:
- accessibleTasksCache (45с) per userId:orgId внутри storage-метода
- инвалидация в storage-слое: роли, делегирования, form access,
  assignees, createTask/updateTask, appRole пользователя
- 9 новых юнит-тестов (65/65 зелёные)
This commit is contained in:
2026-09-07 22:27:33 +03:00
parent 82d577c797
commit 23c49adcd1
9 changed files with 231 additions and 2 deletions

View File

@@ -11,6 +11,7 @@ import { eq, and, or, isNull, isNotNull, sql, desc } from "drizzle-orm";
import { storage } from "../storage";
import { notifyTaskAssigned } from "../utils/notifyAssignee";
import { logAudit } from "../utils/audit";
import { invalidateAccessibleTasksForUser } from "../utils/cache";
/**
* Сервис замещения пользователей (delegation/substitution, референс — Pyrus).
@@ -279,6 +280,7 @@ export async function cleanupStaleDelegates(taskId: number, organizationId: numb
const activeDelegateId = await getActiveDelegate(row.delegatedFromUserId as number, task.formId, organizationId);
if (activeDelegateId !== row.userId) {
await db.delete(taskAssignees).where(eq(taskAssignees.id, row.id));
invalidateAccessibleTasksForUser(row.userId, organizationId);
}
}
} catch (err) {
@@ -360,6 +362,9 @@ export async function createDelegation(data: {
createdBy: data.createdBy,
})
.returning();
// Замещение меняет доступ заместителя (и потенциально отсутствующего)
invalidateAccessibleTasksForUser(data.userId, data.organizationId);
invalidateAccessibleTasksForUser(data.delegateUserId, data.organizationId);
return { conflict: false, delegation };
}
@@ -380,6 +385,10 @@ export async function updateDelegation(
)
)
.returning();
if (updated) {
invalidateAccessibleTasksForUser(userId, organizationId);
invalidateAccessibleTasksForUser(updated.delegateUserId, organizationId);
}
return updated ?? null;
}
@@ -397,7 +406,11 @@ export async function deleteDelegation(
eq(userDelegations.organizationId, organizationId)
)
)
.returning({ id: userDelegations.id });
.returning({ id: userDelegations.id, delegateUserId: userDelegations.delegateUserId });
if (deleted.length > 0) {
invalidateAccessibleTasksForUser(userId, organizationId);
invalidateAccessibleTasksForUser(deleted[0].delegateUserId, organizationId);
}
return deleted.length > 0;
}