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

155 lines
4.5 KiB
TypeScript

import { ConvexError, v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { requireStaff } from "./rbac";
function normalizeSlug(input?: string | null): string | undefined {
if (!input) return undefined
const trimmed = input.trim()
if (!trimmed) return undefined
const ascii = trimmed
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[\u2013\u2014]/g, "-")
const sanitized = ascii.replace(/[^\w\s-]/g, "").replace(/[_\s]+/g, "-")
const collapsed = sanitized.replace(/-+/g, "-").toLowerCase()
const normalized = collapsed.replace(/^-+|-+$/g, "")
return normalized || undefined
}
export const list = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
await requireStaff(ctx, viewerId, tenantId)
const companies = await ctx.db
.query("companies")
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
.take(200)
return companies.map((c) => ({ id: c._id, name: c.name, slug: c.slug }))
},
})
export const ensureProvisioned = mutation({
args: {
tenantId: v.string(),
slug: v.string(),
name: v.string(),
provisioningCode: v.string(),
},
handler: async (ctx, { tenantId, slug, name, provisioningCode }) => {
const normalizedSlug = normalizeSlug(slug)
if (!normalizedSlug) {
throw new ConvexError("Slug inválido")
}
const trimmedName = name.trim()
if (!trimmedName) {
throw new ConvexError("Nome inválido")
}
const existing = await ctx.db
.query("companies")
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", normalizedSlug))
.unique()
if (existing) {
if (existing.provisioningCode !== provisioningCode) {
await ctx.db.patch(existing._id, { provisioningCode })
}
return {
id: existing._id,
slug: existing.slug,
name: existing.name,
}
}
const now = Date.now()
const id = await ctx.db.insert("companies", {
tenantId,
name: trimmedName,
slug: normalizedSlug,
provisioningCode,
isAvulso: false,
contractedHoursPerMonth: undefined,
cnpj: undefined,
domain: undefined,
phone: undefined,
description: undefined,
address: undefined,
legalName: undefined,
tradeName: undefined,
stateRegistration: undefined,
stateRegistrationType: undefined,
primaryCnae: undefined,
timezone: undefined,
businessHours: undefined,
supportEmail: undefined,
billingEmail: undefined,
contactPreferences: undefined,
clientDomains: undefined,
communicationChannels: undefined,
fiscalAddress: undefined,
hasBranches: false,
regulatedEnvironments: undefined,
privacyPolicyAccepted: false,
privacyPolicyReference: undefined,
privacyPolicyMetadata: undefined,
contracts: undefined,
contacts: undefined,
locations: undefined,
sla: undefined,
tags: undefined,
customFields: undefined,
notes: undefined,
createdAt: now,
updatedAt: now,
})
return {
id,
slug: normalizedSlug,
name: trimmedName,
}
},
})
export const removeBySlug = mutation({
args: {
tenantId: v.string(),
slug: v.string(),
},
handler: async (ctx, { tenantId, slug }) => {
const normalizedSlug = normalizeSlug(slug) ?? slug
const existing = await ctx.db
.query("companies")
.withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", normalizedSlug))
.unique()
if (!existing) {
return { removed: false }
}
// Preserve company snapshot on related tickets before deletion
const relatedTickets = await ctx.db
.query("tickets")
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", existing._id))
.take(200)
if (relatedTickets.length > 0) {
const companySnapshot = {
name: existing.name,
slug: existing.slug,
isAvulso: existing.isAvulso ?? undefined,
}
for (const t of relatedTickets) {
const needsPatch = !t.companySnapshot ||
t.companySnapshot.name !== companySnapshot.name ||
t.companySnapshot.slug !== companySnapshot.slug ||
Boolean(t.companySnapshot.isAvulso ?? false) !== Boolean(companySnapshot.isAvulso ?? false)
if (needsPatch) {
await ctx.db.patch(t._id, { companySnapshot })
}
}
}
await ctx.db.delete(existing._id)
return { removed: true }
},
})