sistema-de-chamados/convex/ticketFormSettings.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

158 lines
5 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"
import { getTemplateByKey, normalizeFormTemplateKey } from "./ticketFormTemplates"
import { TICKET_FORM_CONFIG } from "./ticketForms.config"
const VALID_SCOPES = new Set(["tenant", "company", "user"])
function normalizeScope(input: string) {
const normalized = input.trim().toLowerCase()
if (!VALID_SCOPES.has(normalized)) {
throw new ConvexError("Escopo inválido")
}
return normalized
}
async function ensureTemplateExists(ctx: MutationCtx | QueryCtx, tenantId: string, template: string) {
const normalized = normalizeFormTemplateKey(template)
if (!normalized) {
throw new ConvexError("Template desconhecido")
}
const existing = await getTemplateByKey(ctx, tenantId, normalized)
if (existing && existing.isArchived !== true) {
return normalized
}
const fallback = TICKET_FORM_CONFIG.find((tpl) => tpl.key === normalized)
if (fallback) {
return normalized
}
throw new ConvexError("Template desconhecido")
}
export const list = query({
args: {
tenantId: v.string(),
viewerId: v.id("users"),
template: v.optional(v.string()),
},
handler: async (ctx, { tenantId, viewerId, template }) => {
await requireAdmin(ctx, viewerId, tenantId)
const normalizedTemplate = template ? normalizeFormTemplateKey(template) : null
const settings = await ctx.db
.query("ticketFormSettings")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.take(100)
return settings
.filter((setting) => !normalizedTemplate || setting.template === normalizedTemplate)
.map((setting) => ({
id: setting._id,
template: setting.template,
scope: setting.scope,
companyId: setting.companyId ?? null,
userId: setting.userId ?? null,
enabled: setting.enabled,
createdAt: setting.createdAt,
updatedAt: setting.updatedAt,
actorId: setting.actorId ?? null,
}))
},
})
export const upsert = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
template: v.string(),
scope: v.string(),
companyId: v.optional(v.id("companies")),
userId: v.optional(v.id("users")),
enabled: v.boolean(),
},
handler: async (ctx, { tenantId, actorId, template, scope, companyId, userId, enabled }) => {
await requireAdmin(ctx, actorId, tenantId)
const normalizedTemplate = await ensureTemplateExists(ctx, tenantId, template)
const normalizedScope = normalizeScope(scope)
if (normalizedScope === "company" && !companyId) {
throw new ConvexError("Informe a empresa para configurar o template")
}
if (normalizedScope === "user" && !userId) {
throw new ConvexError("Informe o usuário para configurar o template")
}
if (normalizedScope === "tenant") {
if (companyId || userId) {
throw new ConvexError("Escopo global não aceita empresa ou usuário")
}
}
const existing = await findExisting(ctx, tenantId, normalizedTemplate, normalizedScope, companyId, userId)
const now = Date.now()
if (existing) {
await ctx.db.patch(existing._id, {
enabled,
updatedAt: now,
actorId,
})
return existing._id
}
const id = await ctx.db.insert("ticketFormSettings", {
tenantId,
template: normalizedTemplate,
scope: normalizedScope,
companyId: normalizedScope === "company" ? (companyId as Id<"companies">) : undefined,
userId: normalizedScope === "user" ? (userId as Id<"users">) : undefined,
enabled,
createdAt: now,
updatedAt: now,
actorId,
})
return id
},
})
export const remove = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
settingId: v.id("ticketFormSettings"),
},
handler: async (ctx, { tenantId, actorId, settingId }) => {
await requireAdmin(ctx, actorId, tenantId)
const setting = await ctx.db.get(settingId)
if (!setting || setting.tenantId !== tenantId) {
throw new ConvexError("Configuração não encontrada")
}
await ctx.db.delete(settingId)
return { ok: true }
},
})
async function findExisting(
ctx: MutationCtx | QueryCtx,
tenantId: string,
template: string,
scope: string,
companyId?: Id<"companies">,
userId?: Id<"users">,
) {
const candidates = await ctx.db
.query("ticketFormSettings")
.withIndex("by_tenant_template_scope", (q) => q.eq("tenantId", tenantId).eq("template", template).eq("scope", scope))
.take(100)
return candidates.find((setting) => {
if (scope === "tenant") return true
if (scope === "company") {
return setting.companyId && companyId && String(setting.companyId) === String(companyId)
}
if (scope === "user") {
return setting.userId && userId && String(setting.userId) === String(userId)
}
return false
}) ?? null
}