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>
155 lines
4.5 KiB
TypeScript
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 }
|
|
},
|
|
})
|