sistema-de-chamados/convex/slas.ts
esdrasrenan 638faeb287 fix(convex): corrigir memory leak com .collect() sem limite e adicionar otimizacoes
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>
2025-12-09 21:41:30 -03:00

137 lines
4.4 KiB
TypeScript

import { mutation, query } from "./_generated/server";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import type { Id } from "./_generated/dataModel";
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<"slaPolicies">) {
const existing = await ctx.db
.query("slaPolicies")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId).eq("name", name))
.first();
if (existing && (!excludeId || existing._id !== excludeId)) {
throw new ConvexError("Já existe uma política SLA 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 items = await ctx.db
.query("slaPolicies")
.withIndex("by_tenant_name", (q) => q.eq("tenantId", tenantId))
.take(50);
return items.map((policy) => ({
id: policy._id,
name: policy.name,
description: policy.description ?? "",
timeToFirstResponse: policy.timeToFirstResponse ?? null,
timeToResolution: policy.timeToResolution ?? null,
}));
},
});
export const create = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
name: v.string(),
description: v.optional(v.string()),
timeToFirstResponse: v.optional(v.number()),
timeToResolution: v.optional(v.number()),
},
handler: async (ctx, { tenantId, actorId, name, description, timeToFirstResponse, timeToResolution }) => {
await requireAdmin(ctx, actorId, tenantId);
const trimmed = normalizeName(name);
if (trimmed.length < 2) {
throw new ConvexError("Informe um nome válido para a política");
}
await ensureUniqueName(ctx, tenantId, trimmed);
if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) {
throw new ConvexError("Tempo para primeira resposta deve ser positivo");
}
if (timeToResolution !== undefined && timeToResolution < 0) {
throw new ConvexError("Tempo para resolução deve ser positivo");
}
const id = await ctx.db.insert("slaPolicies", {
tenantId,
name: trimmed,
description,
timeToFirstResponse,
timeToResolution,
});
return id;
},
});
export const update = mutation({
args: {
policyId: v.id("slaPolicies"),
tenantId: v.string(),
actorId: v.id("users"),
name: v.string(),
description: v.optional(v.string()),
timeToFirstResponse: v.optional(v.number()),
timeToResolution: v.optional(v.number()),
},
handler: async (ctx, { policyId, tenantId, actorId, name, description, timeToFirstResponse, timeToResolution }) => {
await requireAdmin(ctx, actorId, tenantId);
const policy = await ctx.db.get(policyId);
if (!policy || policy.tenantId !== tenantId) {
throw new ConvexError("Política não encontrada");
}
const trimmed = normalizeName(name);
if (trimmed.length < 2) {
throw new ConvexError("Informe um nome válido para a política");
}
if (timeToFirstResponse !== undefined && timeToFirstResponse < 0) {
throw new ConvexError("Tempo para primeira resposta deve ser positivo");
}
if (timeToResolution !== undefined && timeToResolution < 0) {
throw new ConvexError("Tempo para resolução deve ser positivo");
}
await ensureUniqueName(ctx, tenantId, trimmed, policyId);
await ctx.db.patch(policyId, {
name: trimmed,
description,
timeToFirstResponse,
timeToResolution,
});
},
});
export const remove = mutation({
args: {
policyId: v.id("slaPolicies"),
tenantId: v.string(),
actorId: v.id("users"),
},
handler: async (ctx, { policyId, tenantId, actorId }) => {
await requireAdmin(ctx, actorId, tenantId);
const policy = await ctx.db.get(policyId);
if (!policy || policy.tenantId !== tenantId) {
throw new ConvexError("Política não encontrada");
}
const ticketLinked = await ctx.db
.query("tickets")
.withIndex("by_tenant_sla_policy", (q) => q.eq("tenantId", tenantId).eq("slaPolicyId", policyId))
.first();
if (ticketLinked) {
throw new ConvexError("Remova a associação de tickets antes de excluir a política");
}
await ctx.db.delete(policyId);
},
});