Files
clube67_newwhats.local/clube67/newwhats.local/backend/src/modules/chats/chat.repository.ts
T

214 lines
6.7 KiB
TypeScript

import { Prisma } from '@prisma/client'
import { prisma } from '../../infra/database/prisma'
interface ContactSlim {
id: string
name: string | null
notify: string | null
verifiedName: string | null
phone: string | null
avatarUrl: string | null
scoreReputacao: number
flagRestricao: boolean
}
interface ProtocolSlim {
chatId: string
number: string
status: string
}
interface LastMsgSlim {
chatId: string
body: string | null
fromMe: boolean
status: string
timestamp: Date
type: string
pushName: string | null
}
export class ChatRepository {
async findAllByInstance(tenantId: string, instanceId: string, opts: {
limit?: number
search?: string
archived?: boolean
} = {}) {
const { limit = 200, search, archived = false } = opts
// ── Q1: chats (sem includes — bate o N+1) ────────────────────────────────
const chats = await prisma.chat.findMany({
where: {
tenantId,
instanceId,
isArchived: archived,
NOT: { OR: [
{ jid: { endsWith: '@lid' } },
{ jid: { contains: '@broadcast' } },
{ jid: { endsWith: '@newsletter' } },
]},
...(search
? {
OR: [
{ jid: { contains: search, mode: 'insensitive' as const } },
{ contact: { name: { contains: search, mode: 'insensitive' as const } } },
{ contact: { notify: { contains: search, mode: 'insensitive' as const } } },
{ contact: { phone: { contains: search, mode: 'insensitive' as const } } },
],
}
: {}),
},
orderBy: [
{ isPinned: 'desc' },
{ lastMessageAt: { sort: 'desc', nulls: 'last' } },
{ createdAt: 'desc' },
],
take: limit,
})
if (chats.length === 0) {
return [] as Array<typeof chats[number] & {
contact: ContactSlim | null
protocols: Array<{ number: string; status: string }>
messages: Array<Omit<LastMsgSlim, 'chatId'>>
}>
}
const chatIds = chats.map((c) => c.id)
const contactIds = Array.from(new Set(chats.map((c) => c.contactId).filter((x): x is string => !!x)))
// ── Q2/Q3/Q4 em paralelo: contatos, protocols (1 por chat), last msg (1 por chat) ──
const [contacts, protocols, lastMsgs] = await Promise.all([
contactIds.length > 0
? prisma.contact.findMany({
where: { id: { in: contactIds } },
select: {
id: true,
name: true,
notify: true,
verifiedName: true,
phone: true,
avatarUrl: true,
scoreReputacao: true,
flagRestricao: true,
},
})
: Promise.resolve([] as ContactSlim[]),
// DISTINCT ON: 1 query batch — pega o protocolo aberto mais recente por chat
prisma.$queryRaw<ProtocolSlim[]>`
SELECT DISTINCT ON ("chatId") "chatId", number, status::text AS status
FROM protocols
WHERE "chatId" = ANY(${chatIds}::text[])
AND status IN ('OPEN','IN_PROGRESS','WAITING_CLIENT','WAITING_AGENT')
ORDER BY "chatId", "createdAt" DESC
`,
// DISTINCT ON: 1 query batch — pega a última msg por chat
prisma.$queryRaw<LastMsgSlim[]>`
SELECT DISTINCT ON ("chatId") "chatId", body, "fromMe", status::text AS status, timestamp, type::text AS type, "pushName"
FROM messages
WHERE "chatId" = ANY(${chatIds}::text[])
ORDER BY "chatId", timestamp DESC
`,
])
// ── Indexa para lookup O(1) ──────────────────────────────────────────────
const contactById = new Map<string, ContactSlim>(contacts.map((c) => [c.id, c]))
const protocolByChat = new Map<string, ProtocolSlim>(protocols.map((p) => [p.chatId, p]))
const lastMsgByChat = new Map<string, LastMsgSlim>(lastMsgs.map((m) => [m.chatId, m]))
// ── Re-monta a estrutura no formato esperado pelo chat.routes.ts ─────────
return chats.map((c) => {
const contact = c.contactId ? contactById.get(c.contactId) ?? null : null
const protocol = protocolByChat.get(c.id)
const lastMsg = lastMsgByChat.get(c.id)
return {
...c,
contact: contact
? {
name: contact.name,
notify: contact.notify,
verifiedName: contact.verifiedName,
phone: contact.phone,
avatarUrl: contact.avatarUrl,
scoreReputacao: contact.scoreReputacao,
flagRestricao: contact.flagRestricao,
}
: null,
protocols: protocol
? [{ number: protocol.number, status: protocol.status }]
: [],
messages: lastMsg
? [{
body: lastMsg.body,
fromMe: lastMsg.fromMe,
status: lastMsg.status,
timestamp: lastMsg.timestamp,
type: lastMsg.type,
pushName: lastMsg.pushName,
}]
: [],
}
})
}
async findById(id: string, tenantId: string) {
return prisma.chat.findFirst({
where: { id, tenantId },
include: {
contact: true,
protocols: {
where: { status: { in: ['OPEN', 'IN_PROGRESS', 'WAITING_CLIENT', 'WAITING_AGENT'] } },
take: 1,
orderBy: { createdAt: 'desc' },
},
},
})
}
async findByJid(tenantId: string, instanceId: string, jid: string) {
return prisma.chat.findUnique({
where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } },
include: { contact: true },
})
}
async clearUnread(id: string, tenantId: string) {
return prisma.chat.updateMany({
where: { id, tenantId },
data: { unreadCount: 0 },
})
}
async setArchived(id: string, tenantId: string, archived: boolean) {
return prisma.chat.updateMany({
where: { id, tenantId },
data: { isArchived: archived },
})
}
async setPinned(id: string, tenantId: string, pinned: boolean) {
return prisma.chat.updateMany({
where: { id, tenantId },
data: { isPinned: pinned },
})
}
async countUnreadByInstance(tenantId: string, instanceId: string): Promise<number> {
const result = await prisma.chat.aggregate({
where: {
tenantId,
instanceId,
isArchived: false,
NOT: { OR: [
{ jid: { endsWith: '@lid' } },
{ jid: { contains: '@broadcast' } },
{ jid: { endsWith: '@newsletter' } },
]}
},
_sum: { unreadCount: true },
})
return result._sum.unreadCount ?? 0
}
}