214 lines
6.7 KiB
TypeScript
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
|
|
}
|
|
}
|