Problema: Convex backend consumindo 16GB+ de RAM causando OOM kills Correcoes aplicadas: - Substituido todos os .collect() por .take(LIMIT) em 27+ arquivos - Adicionado indice by_usbPolicyStatus para otimizar query de maquinas - Corrigido N+1 problem em alerts.ts usando Map lookup - Corrigido full table scan em usbPolicy.ts - Corrigido subscription leaks no frontend (tickets-view, use-ticket-categories) - Atualizado versao do Convex backend para precompiled-2025-12-04-cc6af4c Arquivos principais modificados: - convex/*.ts - limites em todas as queries .collect() - convex/schema.ts - novo indice by_usbPolicyStatus - convex/alerts.ts - N+1 fix com Map - convex/usbPolicy.ts - uso do novo indice - src/components/tickets/tickets-view.tsx - skip condicional - src/hooks/use-ticket-categories.ts - skip condicional 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
231 lines
6.7 KiB
TypeScript
231 lines
6.7 KiB
TypeScript
import { mutation, query } from "./_generated/server";
|
|
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
|
import type { Id } from "./_generated/dataModel";
|
|
import { ConvexError, v } from "convex/values";
|
|
|
|
import { requireAdmin } from "./rbac";
|
|
|
|
function normalizeName(value: string) {
|
|
return value.trim();
|
|
}
|
|
|
|
type AnyCtx = QueryCtx | MutationCtx;
|
|
|
|
async function ensureUniqueName(ctx: AnyCtx, tenantId: string, name: string, excludeId?: Id<"teams">) {
|
|
const existing = await ctx.db
|
|
.query("teams")
|
|
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name))
|
|
.first();
|
|
if (existing && (!excludeId || existing._id !== excludeId)) {
|
|
throw new ConvexError("Já existe um time com este nome");
|
|
}
|
|
}
|
|
|
|
export const list = query({
|
|
args: { tenantId: v.string(), viewerId: v.id("users") },
|
|
handler: async (ctx, { tenantId, viewerId }) => {
|
|
await requireAdmin(ctx, viewerId, tenantId);
|
|
const teams = await ctx.db
|
|
.query("teams")
|
|
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
const users = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
const queues = await ctx.db
|
|
.query("queues")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
return teams.map((team) => {
|
|
const members = users
|
|
.filter((user) => (user.teams ?? []).includes(team.name))
|
|
.map((user) => ({
|
|
id: user._id,
|
|
name: user.name,
|
|
email: user.email,
|
|
role: user.role ?? "AGENT",
|
|
}));
|
|
|
|
const linkedQueues = queues.filter((queue) => queue.teamId === team._id);
|
|
|
|
return {
|
|
id: team._id,
|
|
name: team.name,
|
|
description: team.description ?? "",
|
|
members,
|
|
queueCount: linkedQueues.length,
|
|
createdAt: team._creationTime,
|
|
};
|
|
});
|
|
},
|
|
});
|
|
|
|
export const create = mutation({
|
|
args: {
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, { tenantId, actorId, name, description }) => {
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const trimmed = normalizeName(name);
|
|
if (trimmed.length < 2) {
|
|
throw new ConvexError("Informe um nome válido para o time");
|
|
}
|
|
await ensureUniqueName(ctx, tenantId, trimmed);
|
|
const id = await ctx.db.insert("teams", {
|
|
tenantId,
|
|
name: trimmed,
|
|
description,
|
|
});
|
|
return id;
|
|
},
|
|
});
|
|
|
|
export const update = mutation({
|
|
args: {
|
|
teamId: v.id("teams"),
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
name: v.string(),
|
|
description: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, { teamId, tenantId, actorId, name, description }) => {
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const team = await ctx.db.get(teamId);
|
|
if (!team || team.tenantId !== tenantId) {
|
|
throw new ConvexError("Time não encontrado");
|
|
}
|
|
const trimmed = normalizeName(name);
|
|
if (trimmed.length < 2) {
|
|
throw new ConvexError("Informe um nome válido para o time");
|
|
}
|
|
await ensureUniqueName(ctx, tenantId, trimmed, teamId);
|
|
|
|
if (team.name !== trimmed) {
|
|
const users = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
const now = users
|
|
.filter((user) => (user.teams ?? []).includes(team.name))
|
|
.map(async (user) => {
|
|
const teams = (user.teams ?? []).map((entry) => (entry === team.name ? trimmed : entry));
|
|
await ctx.db.patch(user._id, { teams });
|
|
});
|
|
await Promise.all(now);
|
|
}
|
|
|
|
await ctx.db.patch(teamId, { name: trimmed, description });
|
|
},
|
|
});
|
|
|
|
export const remove = mutation({
|
|
args: {
|
|
teamId: v.id("teams"),
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
},
|
|
handler: async (ctx, { teamId, tenantId, actorId }) => {
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const team = await ctx.db.get(teamId);
|
|
if (!team || team.tenantId !== tenantId) {
|
|
throw new ConvexError("Time não encontrado");
|
|
}
|
|
|
|
const queuesLinked = await ctx.db
|
|
.query("queues")
|
|
.withIndex("by_tenant_team", (q) => q.eq("tenantId", tenantId).eq("teamId", teamId))
|
|
.first();
|
|
if (queuesLinked) {
|
|
throw new ConvexError("Remova ou realoque as filas associadas antes de excluir o time");
|
|
}
|
|
|
|
const users = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
await Promise.all(
|
|
users
|
|
.filter((user) => (user.teams ?? []).includes(team.name))
|
|
.map((user) => {
|
|
const teams = (user.teams ?? []).filter((entry) => entry !== team.name);
|
|
return ctx.db.patch(user._id, { teams });
|
|
})
|
|
);
|
|
|
|
await ctx.db.delete(teamId);
|
|
},
|
|
});
|
|
|
|
export const setMembers = mutation({
|
|
args: {
|
|
teamId: v.id("teams"),
|
|
tenantId: v.string(),
|
|
actorId: v.id("users"),
|
|
memberIds: v.array(v.id("users")),
|
|
},
|
|
handler: async (ctx, { teamId, tenantId, actorId, memberIds }) => {
|
|
await requireAdmin(ctx, actorId, tenantId);
|
|
const team = await ctx.db.get(teamId);
|
|
if (!team || team.tenantId !== tenantId) {
|
|
throw new ConvexError("Time não encontrado");
|
|
}
|
|
|
|
const users = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
const tenantUserIds = new Set(users.map((user) => user._id));
|
|
for (const memberId of memberIds) {
|
|
if (!tenantUserIds.has(memberId)) {
|
|
throw new ConvexError("Usuário inválido para este tenant");
|
|
}
|
|
}
|
|
const target = new Set(memberIds);
|
|
|
|
await Promise.all(
|
|
users.map(async (user) => {
|
|
const teams = new Set(user.teams ?? []);
|
|
const hasTeam = teams.has(team.name);
|
|
const shouldHave = target.has(user._id);
|
|
|
|
if (shouldHave && !hasTeam) {
|
|
teams.add(team.name);
|
|
await ctx.db.patch(user._id, { teams: Array.from(teams) });
|
|
}
|
|
|
|
if (!shouldHave && hasTeam) {
|
|
teams.delete(team.name);
|
|
await ctx.db.patch(user._id, { teams: Array.from(teams) });
|
|
}
|
|
})
|
|
);
|
|
},
|
|
});
|
|
|
|
export const directory = query({
|
|
args: { tenantId: v.string(), viewerId: v.id("users") },
|
|
handler: async (ctx, { tenantId, viewerId }) => {
|
|
await requireAdmin(ctx, viewerId, tenantId);
|
|
const users = await ctx.db
|
|
.query("users")
|
|
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
|
.take(50);
|
|
|
|
return users.map((user) => ({
|
|
id: user._id,
|
|
name: user.name,
|
|
email: user.email,
|
|
role: user.role ?? "AGENT",
|
|
teams: user.teams ?? [],
|
|
}));
|
|
},
|
|
});
|