evolution-api/.cursor/rules/specialized-rules/validate-rules.mdc
Davidson Gomes 7088ad05d2 feat: add project guidelines and configuration files for development standards
- Introduce AGENTS.md for repository guidelines and project structure
- Add core development principles in .cursor/rules/core-development.mdc
- Establish project-specific context in .cursor/rules/project-context.mdc
- Implement Cursor IDE configuration in .cursor/rules/cursor.json
- Create specialized rules for controllers, services, DTOs, guards, routes, and integrations
- Update .gitignore to exclude unnecessary files
- Enhance CLAUDE.md with project overview and common development commands
2025-09-17 15:43:32 -03:00

498 lines
15 KiB
Plaintext

---
description: Validation schemas and patterns for Evolution API
globs:
- "src/validate/**/*.ts"
alwaysApply: false
---
# Evolution API Validation Rules
## Validation Schema Structure
### JSONSchema7 Pattern (Evolution API Standard)
```typescript
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...fields: string[]) => {
const properties = {};
fields.forEach((field) => {
properties[field] = {
if: { properties: { [field]: { type: 'string' } } },
then: { properties: { [field]: { minLength: 1 } } },
};
});
return {
allOf: Object.values(properties),
};
};
export const exampleSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
enabled: { type: 'boolean' },
settings: {
type: 'object',
properties: {
timeout: { type: 'number', minimum: 1000, maximum: 60000 },
retries: { type: 'number', minimum: 0, maximum: 5 },
},
},
tags: {
type: 'array',
items: { type: 'string' },
},
},
required: ['name', 'enabled'],
...isNotEmpty('name'),
};
```
## Message Validation Schemas
### Send Message Validation
```typescript
const numberDefinition = {
type: 'string',
pattern: '^\\d+[\\.@\\w-]+',
description: 'Invalid number',
};
export const sendTextSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: numberDefinition,
text: { type: 'string', minLength: 1, maxLength: 4096 },
delay: { type: 'number', minimum: 0, maximum: 60000 },
linkPreview: { type: 'boolean' },
mentionsEveryOne: { type: 'boolean' },
mentioned: {
type: 'array',
items: { type: 'string' },
},
},
required: ['number', 'text'],
...isNotEmpty('number', 'text'),
};
export const sendMediaSchema = Joi.object({
number: Joi.string().required().pattern(/^\d+$/),
mediatype: Joi.string().required().valid('image', 'video', 'audio', 'document'),
media: Joi.alternatives().try(
Joi.string().uri(),
Joi.string().base64(),
).required(),
caption: Joi.string().optional().max(1024),
fileName: Joi.string().optional().max(255),
delay: Joi.number().optional().min(0).max(60000),
}).required();
export const sendButtonsSchema = Joi.object({
number: Joi.string().required().pattern(/^\d+$/),
title: Joi.string().required().max(1024),
description: Joi.string().optional().max(1024),
footer: Joi.string().optional().max(60),
buttons: Joi.array().items(
Joi.object({
type: Joi.string().required().valid('replyButton', 'urlButton', 'callButton'),
displayText: Joi.string().required().max(20),
id: Joi.string().when('type', {
is: 'replyButton',
then: Joi.required().max(256),
otherwise: Joi.optional(),
}),
url: Joi.string().when('type', {
is: 'urlButton',
then: Joi.required().uri(),
otherwise: Joi.optional(),
}),
phoneNumber: Joi.string().when('type', {
is: 'callButton',
then: Joi.required().pattern(/^\+?\d+$/),
otherwise: Joi.optional(),
}),
})
).min(1).max(3).required(),
}).required();
```
## Instance Validation Schemas
### Instance Creation Validation
```typescript
export const instanceSchema = Joi.object({
instanceName: Joi.string().required().min(1).max(100).pattern(/^[a-zA-Z0-9_-]+$/),
integration: Joi.string().required().valid('WHATSAPP-BAILEYS', 'WHATSAPP-BUSINESS', 'EVOLUTION'),
token: Joi.string().when('integration', {
is: Joi.valid('WHATSAPP-BUSINESS', 'EVOLUTION'),
then: Joi.required().min(10),
otherwise: Joi.optional(),
}),
qrcode: Joi.boolean().optional().default(false),
number: Joi.string().optional().pattern(/^\d+$/),
businessId: Joi.string().when('integration', {
is: 'WHATSAPP-BUSINESS',
then: Joi.required(),
otherwise: Joi.optional(),
}),
}).required();
export const settingsSchema = Joi.object({
rejectCall: Joi.boolean().optional(),
msgCall: Joi.string().optional().max(500),
groupsIgnore: Joi.boolean().optional(),
alwaysOnline: Joi.boolean().optional(),
readMessages: Joi.boolean().optional(),
readStatus: Joi.boolean().optional(),
syncFullHistory: Joi.boolean().optional(),
wavoipToken: Joi.string().optional(),
}).optional();
export const proxySchema = Joi.object({
host: Joi.string().required().hostname(),
port: Joi.string().required().pattern(/^\d+$/).custom((value) => {
const port = parseInt(value);
if (port < 1 || port > 65535) {
throw new Error('Port must be between 1 and 65535');
}
return value;
}),
protocol: Joi.string().required().valid('http', 'https', 'socks4', 'socks5'),
username: Joi.string().optional(),
password: Joi.string().optional(),
}).optional();
```
## Webhook Validation Schemas
### Webhook Configuration Validation
```typescript
export const webhookSchema = Joi.object({
enabled: Joi.boolean().required(),
url: Joi.string().when('enabled', {
is: true,
then: Joi.required().uri({ scheme: ['http', 'https'] }),
otherwise: Joi.optional(),
}),
events: Joi.array().items(
Joi.string().valid(
'APPLICATION_STARTUP',
'INSTANCE_CREATE',
'INSTANCE_DELETE',
'QRCODE_UPDATED',
'CONNECTION_UPDATE',
'STATUS_INSTANCE',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
'CHATS_SET',
'CHATS_UPDATE',
'CHATS_UPSERT',
'CHATS_DELETE',
'GROUPS_UPSERT',
'GROUPS_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CALL'
)
).min(1).when('enabled', {
is: true,
then: Joi.required(),
otherwise: Joi.optional(),
}),
headers: Joi.object().pattern(
Joi.string(),
Joi.string()
).optional(),
byEvents: Joi.boolean().optional().default(false),
base64: Joi.boolean().optional().default(false),
}).required();
```
## Chatbot Validation Schemas
### Base Chatbot Validation
```typescript
export const baseChatbotSchema = Joi.object({
enabled: Joi.boolean().required(),
description: Joi.string().required().min(1).max(500),
expire: Joi.number().optional().min(0).max(86400), // 24 hours in seconds
keywordFinish: Joi.string().optional().max(100),
delayMessage: Joi.number().optional().min(0).max(10000),
unknownMessage: Joi.string().optional().max(1000),
listeningFromMe: Joi.boolean().optional().default(false),
stopBotFromMe: Joi.boolean().optional().default(false),
keepOpen: Joi.boolean().optional().default(false),
debounceTime: Joi.number().optional().min(0).max(60000),
triggerType: Joi.string().required().valid('ALL', 'KEYWORD', 'REGEX'),
triggerOperator: Joi.string().when('triggerType', {
is: 'KEYWORD',
then: Joi.required().valid('EQUALS', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH'),
otherwise: Joi.optional(),
}),
triggerValue: Joi.string().when('triggerType', {
is: Joi.valid('KEYWORD', 'REGEX'),
then: Joi.required().min(1).max(500),
otherwise: Joi.optional(),
}),
ignoreJids: Joi.array().items(Joi.string()).optional(),
splitMessages: Joi.boolean().optional().default(false),
timePerChar: Joi.number().optional().min(10).max(1000).default(100),
}).required();
export const typebotSchema = baseChatbotSchema.keys({
url: Joi.string().required().uri({ scheme: ['http', 'https'] }),
typebot: Joi.string().required().min(1).max(100),
apiVersion: Joi.string().optional().valid('v1', 'v2').default('v1'),
}).required();
export const openaiSchema = baseChatbotSchema.keys({
apiKey: Joi.string().required().min(10),
model: Joi.string().optional().valid(
'gpt-3.5-turbo',
'gpt-3.5-turbo-16k',
'gpt-4',
'gpt-4-32k',
'gpt-4-turbo-preview'
).default('gpt-3.5-turbo'),
systemMessage: Joi.string().optional().max(2000),
maxTokens: Joi.number().optional().min(1).max(4000).default(1000),
temperature: Joi.number().optional().min(0).max(2).default(0.7),
}).required();
```
## Business API Validation Schemas
### Template Validation
```typescript
export const templateSchema = Joi.object({
name: Joi.string().required().min(1).max(512).pattern(/^[a-z0-9_]+$/),
category: Joi.string().required().valid('MARKETING', 'UTILITY', 'AUTHENTICATION'),
allowCategoryChange: Joi.boolean().required(),
language: Joi.string().required().pattern(/^[a-z]{2}_[A-Z]{2}$/), // e.g., pt_BR, en_US
components: Joi.array().items(
Joi.object({
type: Joi.string().required().valid('HEADER', 'BODY', 'FOOTER', 'BUTTONS'),
format: Joi.string().when('type', {
is: 'HEADER',
then: Joi.valid('TEXT', 'IMAGE', 'VIDEO', 'DOCUMENT'),
otherwise: Joi.optional(),
}),
text: Joi.string().when('type', {
is: Joi.valid('HEADER', 'BODY', 'FOOTER'),
then: Joi.required().min(1).max(1024),
otherwise: Joi.optional(),
}),
buttons: Joi.array().when('type', {
is: 'BUTTONS',
then: Joi.items(
Joi.object({
type: Joi.string().required().valid('QUICK_REPLY', 'URL', 'PHONE_NUMBER'),
text: Joi.string().required().min(1).max(25),
url: Joi.string().when('type', {
is: 'URL',
then: Joi.required().uri(),
otherwise: Joi.optional(),
}),
phone_number: Joi.string().when('type', {
is: 'PHONE_NUMBER',
then: Joi.required().pattern(/^\+?\d+$/),
otherwise: Joi.optional(),
}),
})
).min(1).max(10),
otherwise: Joi.optional(),
}),
})
).min(1).required(),
webhookUrl: Joi.string().optional().uri({ scheme: ['http', 'https'] }),
}).required();
export const catalogSchema = Joi.object({
number: Joi.string().optional().pattern(/^\d+$/),
limit: Joi.number().optional().min(1).max(1000).default(10),
cursor: Joi.string().optional(),
}).optional();
```
## Group Validation Schemas
### Group Management Validation
```typescript
export const createGroupSchema = Joi.object({
subject: Joi.string().required().min(1).max(100),
description: Joi.string().optional().max(500),
participants: Joi.array().items(
Joi.string().pattern(/^\d+$/)
).min(1).max(256).required(),
promoteParticipants: Joi.boolean().optional().default(false),
}).required();
export const updateGroupSchema = Joi.object({
subject: Joi.string().optional().min(1).max(100),
description: Joi.string().optional().max(500),
}).min(1).required();
export const groupParticipantsSchema = Joi.object({
participants: Joi.array().items(
Joi.string().pattern(/^\d+$/)
).min(1).max(50).required(),
action: Joi.string().required().valid('add', 'remove', 'promote', 'demote'),
}).required();
```
## Label Validation Schemas
### Label Management Validation
```typescript
export const labelSchema = Joi.object({
name: Joi.string().required().min(1).max(100),
color: Joi.string().required().pattern(/^#[0-9A-Fa-f]{6}$/), // Hex color
predefinedId: Joi.string().optional(),
}).required();
export const handleLabelSchema = Joi.object({
number: Joi.string().required().pattern(/^\d+$/),
labelId: Joi.string().required(),
action: Joi.string().required().valid('add', 'remove'),
}).required();
```
## Custom Validation Functions
### Phone Number Validation
```typescript
export function validatePhoneNumber(number: string): boolean {
// Remove any non-digit characters
const cleaned = number.replace(/\D/g, '');
// Check minimum and maximum length
if (cleaned.length < 10 || cleaned.length > 15) {
return false;
}
// Check for valid country codes (basic validation)
const validCountryCodes = ['1', '7', '20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '51', '52', '53', '54', '55', '56', '57', '58', '60', '61', '62', '63', '64', '65', '66', '81', '82', '84', '86', '90', '91', '92', '93', '94', '95', '98'];
// Check if starts with valid country code
const startsWithValidCode = validCountryCodes.some(code => cleaned.startsWith(code));
return startsWithValidCode;
}
export const phoneNumberValidator = Joi.string().custom((value, helpers) => {
if (!validatePhoneNumber(value)) {
return helpers.error('any.invalid');
}
return value;
}, 'Phone number validation');
```
### Base64 Validation
```typescript
export function validateBase64(base64: string): boolean {
try {
// Check if it's a valid base64 string
const decoded = Buffer.from(base64, 'base64').toString('base64');
return decoded === base64;
} catch {
return false;
}
}
export const base64Validator = Joi.string().custom((value, helpers) => {
if (!validateBase64(value)) {
return helpers.error('any.invalid');
}
return value;
}, 'Base64 validation');
```
### URL Validation with Protocol Check
```typescript
export function validateWebhookUrl(url: string): boolean {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
export const webhookUrlValidator = Joi.string().custom((value, helpers) => {
if (!validateWebhookUrl(value)) {
return helpers.error('any.invalid');
}
return value;
}, 'Webhook URL validation');
```
## Validation Error Handling
### Error Message Customization
```typescript
export const validationMessages = {
'any.required': 'O campo {#label} é obrigatório',
'string.empty': 'O campo {#label} não pode estar vazio',
'string.min': 'O campo {#label} deve ter pelo menos {#limit} caracteres',
'string.max': 'O campo {#label} deve ter no máximo {#limit} caracteres',
'string.pattern.base': 'O campo {#label} possui formato inválido',
'number.min': 'O campo {#label} deve ser maior ou igual a {#limit}',
'number.max': 'O campo {#label} deve ser menor ou igual a {#limit}',
'array.min': 'O campo {#label} deve ter pelo menos {#limit} itens',
'array.max': 'O campo {#label} deve ter no máximo {#limit} itens',
'any.only': 'O campo {#label} deve ser um dos valores: {#valids}',
};
export function formatValidationError(error: Joi.ValidationError): any {
return {
message: 'Dados de entrada inválidos',
details: error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message,
value: detail.context?.value,
})),
};
}
```
## Schema Composition
### Reusable Schema Components
```typescript
export const commonFields = {
instanceName: Joi.string().required().min(1).max(100).pattern(/^[a-zA-Z0-9_-]+$/),
number: phoneNumberValidator.required(),
delay: Joi.number().optional().min(0).max(60000),
enabled: Joi.boolean().optional().default(true),
};
export const mediaFields = {
mediatype: Joi.string().required().valid('image', 'video', 'audio', 'document'),
media: Joi.alternatives().try(
Joi.string().uri(),
base64Validator,
).required(),
caption: Joi.string().optional().max(1024),
fileName: Joi.string().optional().max(255),
};
// Compose schemas using common fields
export const quickMessageSchema = Joi.object({
...commonFields,
text: Joi.string().required().min(1).max(4096),
}).required();
export const quickMediaSchema = Joi.object({
...commonFields,
...mediaFields,
}).required();
```