diff --git a/prisma/mysql-migrations/20261029040337_add_label_instance_unique_key/migration.sql b/prisma/mysql-migrations/20261029040337_add_label_instance_unique_key/migration.sql new file mode 100644 index 0000000000..f0c51abc10 --- /dev/null +++ b/prisma/mysql-migrations/20261029040337_add_label_instance_unique_key/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE UNIQUE INDEX `Label_labelId_instanceId_key` ON `Label`(`labelId`, `instanceId`); diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 255c195dc4..debbad1b18 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -250,6 +250,8 @@ model Label { updatedAt DateTime @updatedAt @db.Timestamp Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String + + @@unique([labelId, instanceId]) } model Proxy { diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 098654c2d0..33d3a4e730 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -23,6 +23,7 @@ import { Events, wa } from '@api/types/wa.types'; import { AudioConverter, Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config'; import { BadRequestException, InternalServerErrorException } from '@exceptions'; import { createJid } from '@utils/createJid'; +import { jsonPath } from '@utils/jsonPath'; import { status } from '@utils/renderStatus'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; @@ -864,7 +865,7 @@ export class BusinessStartupService extends ChannelStartupService { where: { instanceId: this.instanceId, key: { - path: ['id'], + path: jsonPath('id'), equals: key.id, }, }, diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 22839fd451..328c2be4b3 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -85,6 +85,7 @@ import { createId as cuid } from '@paralleldrive/cuid2'; import { Instance, Message } from '@prisma/client'; import { createJid } from '@utils/createJid'; import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion'; +import { jsonPath } from '@utils/jsonPath'; import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent'; import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache'; import { status } from '@utils/renderStatus'; @@ -4184,7 +4185,7 @@ export class BaileysStartupService extends ChannelStartupService { } public async getLastMessage(number: string) { - const where: any = { key: { path: ['remoteJid'], equals: number }, instanceId: this.instanceId }; + const where: any = { key: { path: jsonPath('remoteJid'), equals: number }, instanceId: this.instanceId }; const messages = await this.prismaRepository.message.findMany({ where, @@ -4271,7 +4272,7 @@ export class BaileysStartupService extends ChannelStartupService { if (messageId) { const isLogicalDeleted = configService.get('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE; let message = await this.prismaRepository.message.findFirst({ - where: { key: { path: ['id'], equals: messageId } }, + where: { key: { path: jsonPath('id'), equals: messageId } }, }); if (isLogicalDeleted) { if (!message) return response; @@ -4691,7 +4692,7 @@ export class BaileysStartupService extends ChannelStartupService { const messageId = messageSent.message?.protocolMessage?.key?.id; if (messageId && this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE) { let message = await this.prismaRepository.message.findFirst({ - where: { key: { path: ['id'], equals: messageId } }, + where: { key: { path: jsonPath('id'), equals: messageId } }, }); if (!message) throw new NotFoundException('Message not found'); @@ -5329,10 +5330,10 @@ export class BaileysStartupService extends ChannelStartupService { `INSERT INTO Chat (id, instanceId, remoteJid, labels, createdAt, updatedAt) VALUES (?, ?, ?, JSON_ARRAY(?), NOW(), NOW()) ON DUPLICATE KEY UPDATE - labels = JSON_ARRAY_APPEND( - COALESCE(labels, JSON_ARRAY()), - '$', - ? + labels = IF( + JSON_CONTAINS(COALESCE(labels, JSON_ARRAY()), JSON_QUOTE(?)), + labels, + JSON_ARRAY_APPEND(COALESCE(labels, JSON_ARRAY()), '$', ?) ), updatedAt = NOW()`, id, @@ -5340,6 +5341,7 @@ export class BaileysStartupService extends ChannelStartupService { chatId, labelId, labelId, + labelId, ); } else { // PostgreSQL version @@ -5375,18 +5377,17 @@ export class BaileysStartupService extends ChannelStartupService { `INSERT INTO Chat (id, instanceId, remoteJid, labels, createdAt, updatedAt) VALUES (?, ?, ?, JSON_ARRAY(), NOW(), NOW()) ON DUPLICATE KEY UPDATE - labels = COALESCE( - JSON_REMOVE( - labels, - JSON_UNQUOTE(JSON_SEARCH(labels, 'one', ?)) - ), - JSON_ARRAY() + labels = IF( + JSON_CONTAINS(COALESCE(labels, JSON_ARRAY()), JSON_QUOTE(?)), + JSON_REMOVE(labels, JSON_UNQUOTE(JSON_SEARCH(labels, 'one', ?, '!'))), + COALESCE(labels, JSON_ARRAY()) ), updatedAt = NOW()`, id, instanceId, chatId, labelId, + labelId.replace(/[!%_]/g, '!$&'), ); } else { // PostgreSQL version @@ -5622,13 +5623,15 @@ export class BaileysStartupService extends ChannelStartupService { messageType: query?.where?.messageType, ...timestampFilter, AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {}, + keyFilters?.id ? { key: { path: jsonPath('id'), equals: keyFilters?.id } } : {}, + keyFilters?.fromMe ? { key: { path: jsonPath('fromMe'), equals: keyFilters?.fromMe } } : {}, + keyFilters?.participant ? { key: { path: jsonPath('participant'), equals: keyFilters?.participant } } : {}, { OR: [ - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {}, + keyFilters?.remoteJid ? { key: { path: jsonPath('remoteJid'), equals: keyFilters?.remoteJid } } : {}, + keyFilters?.remoteJidAlt + ? { key: { path: jsonPath('remoteJidAlt'), equals: keyFilters?.remoteJidAlt } } + : {}, ], }, ], @@ -5651,13 +5654,15 @@ export class BaileysStartupService extends ChannelStartupService { messageType: query?.where?.messageType, ...timestampFilter, AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.participant ? { key: { path: ['participant'], equals: keyFilters?.participant } } : {}, + keyFilters?.id ? { key: { path: jsonPath('id'), equals: keyFilters?.id } } : {}, + keyFilters?.fromMe ? { key: { path: jsonPath('fromMe'), equals: keyFilters?.fromMe } } : {}, + keyFilters?.participant ? { key: { path: jsonPath('participant'), equals: keyFilters?.participant } } : {}, { OR: [ - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.remoteJidAlt ? { key: { path: ['remoteJidAlt'], equals: keyFilters?.remoteJidAlt } } : {}, + keyFilters?.remoteJid ? { key: { path: jsonPath('remoteJid'), equals: keyFilters?.remoteJid } } : {}, + keyFilters?.remoteJidAlt + ? { key: { path: jsonPath('remoteJidAlt'), equals: keyFilters?.remoteJidAlt } } + : {}, ], }, ], @@ -5963,7 +5968,7 @@ export class BaileysStartupService extends ChannelStartupService { const messages = await this.prismaRepository.message.findMany({ where: { instanceId: this.instanceId, - AND: [{ key: { path: ['remoteJid'], not: null } }], + AND: [{ key: { path: jsonPath('remoteJid'), not: null } }], }, orderBy: { messageTimestamp: 'desc' }, select: { diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index c7a9753aa6..9ee105d9b5 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -21,6 +21,7 @@ import ChatwootClient, { import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request'; import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageModel } from '@prisma/client'; import i18next from '@utils/i18n'; +import { jsonPath } from '@utils/jsonPath'; import { sendTelemetry } from '@utils/sendTelemetry'; import axios from 'axios'; import { WAMessageContent, WAMessageKey } from 'baileys'; @@ -1593,7 +1594,7 @@ export class ChatwootService { const lastMessage = await this.prismaRepository.message.findFirst({ where: { key: { - path: ['fromMe'], + path: jsonPath('fromMe'), equals: false, }, instanceId: instance.instanceId, @@ -1623,7 +1624,7 @@ export class ChatwootService { where: { instanceId: instance.instanceId, key: { - path: ['id'], + path: jsonPath('id'), equals: key.id, }, }, @@ -2212,7 +2213,7 @@ export class ChatwootService { quotedMsg = await this.prismaRepository.message.findFirst({ where: { key: { - path: ['id'], + path: jsonPath('id'), equals: quotedId, }, chatwootMessageId: { @@ -2524,7 +2525,7 @@ export class ChatwootService { await this.prismaRepository.message.deleteMany({ where: { key: { - path: ['id'], + path: jsonPath('id'), equals: body.key.id, }, instanceId: instance.instanceId, @@ -2942,7 +2943,7 @@ export class ChatwootService { where: { Instance: { name: instance.instanceName }, messageTimestamp: { gte: Number(dayjs().subtract(6, 'hours').unix()) }, - AND: ids.map((id) => ({ key: { path: ['id'], not: id } })), + AND: ids.map((id) => ({ key: { path: jsonPath('id'), not: id } })), }, }); diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 3ba03d5a02..e316056f2f 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -14,6 +14,7 @@ import { Logger } from '@config/logger.config'; import { NotFoundException } from '@exceptions'; import { Contact, Message, Prisma } from '@prisma/client'; import { createJid } from '@utils/createJid'; +import { jsonPath } from '@utils/jsonPath'; import { WASocket } from 'baileys'; import { isArray } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; @@ -623,10 +624,10 @@ export class ChannelStartupService { messageType: query?.where?.messageType, ...timestampFilter, AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.participants ? { key: { path: ['participants'], equals: keyFilters?.participants } } : {}, + keyFilters?.id ? { key: { path: jsonPath('id'), equals: keyFilters?.id } } : {}, + keyFilters?.fromMe ? { key: { path: jsonPath('fromMe'), equals: keyFilters?.fromMe } } : {}, + keyFilters?.remoteJid ? { key: { path: jsonPath('remoteJid'), equals: keyFilters?.remoteJid } } : {}, + keyFilters?.participants ? { key: { path: jsonPath('participants'), equals: keyFilters?.participants } } : {}, ], }, }); @@ -647,10 +648,10 @@ export class ChannelStartupService { messageType: query?.where?.messageType, ...timestampFilter, AND: [ - keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {}, - keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {}, - keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {}, - keyFilters?.participants ? { key: { path: ['participants'], equals: keyFilters?.participants } } : {}, + keyFilters?.id ? { key: { path: jsonPath('id'), equals: keyFilters?.id } } : {}, + keyFilters?.fromMe ? { key: { path: jsonPath('fromMe'), equals: keyFilters?.fromMe } } : {}, + keyFilters?.remoteJid ? { key: { path: jsonPath('remoteJid'), equals: keyFilters?.remoteJid } } : {}, + keyFilters?.participants ? { key: { path: jsonPath('participants'), equals: keyFilters?.participants } } : {}, ], }, orderBy: { diff --git a/src/utils/jsonPath.ts b/src/utils/jsonPath.ts new file mode 100644 index 0000000000..1559051ee3 --- /dev/null +++ b/src/utils/jsonPath.ts @@ -0,0 +1,8 @@ +import { configService, Database } from '@config/env.config'; + +/** + * Prisma takes the path of a JSON field filter as an array on PostgreSQL (`['id']`) + * but as a JSONPath string on MySQL (`'$.id'`). + */ +export const jsonPath = (field: string): any => + configService.get('DATABASE').PROVIDER === 'mysql' ? `$.${field}` : [field];