@tiflux/mcp
Version:
TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients
436 lines (364 loc) • 12.3 kB
JavaScript
/**
* CommunicationRepository - Acesso a dados para comunicações internas
*
* Responsabilidades:
* - Abstrai a API do TiFlux para operações de comunicação interna
* - Implementa upload de arquivos com FormData
* - Paginação e busca de comunicações
* - Error handling específico para comunicações
*/
class CommunicationRepository {
constructor(container) {
this.container = container;
this.logger = container.resolve('logger');
this.httpClient = container.resolve('tifluxHttpClient');
this.config = container.resolve('config');
this.communicationMapper = null; // Lazy loading
}
/**
* Cria uma nova comunicação interna
*/
async create(ticketNumber, communicationData) {
const timer = this.logger.startTimer(`repo_create_communication_${ticketNumber}`);
try {
this.logger.debug('Repository: creating internal communication', {
ticketNumber,
hasText: !!communicationData.text,
hasFiles: !!(communicationData.files && communicationData.files.length > 0)
});
// Prepara FormData para multipart/form-data
const formData = this.httpClient.createFormData();
// Adiciona texto da comunicação
if (communicationData.text) {
formData.append('text', communicationData.text);
}
// Adiciona arquivos se existirem
if (communicationData.files && communicationData.files.length > 0) {
communicationData.files.forEach((filePath, index) => {
this.httpClient.addFileToFormData(
formData,
`files[${index}]`, // Campo esperado pela API
filePath
);
});
}
// Faz a requisição
const response = await this.httpClient.post(
`/tickets/${ticketNumber}/internal_communications`,
formData,
{
timeout: 60000, // Timeout maior para upload de arquivos
maxRetries: 1 // Apenas 1 retry para evitar duplicação
}
);
timer();
// Valida resposta da criação
if (response.statusCode >= 400) {
const errorMessage = this._extractAPIErrorMessage(response.data);
throw new APIError(
`Falha ao criar comunicação interna: ${errorMessage}`,
response.statusCode,
response.data
);
}
// Mapeia comunicação criada
const createdCommunication = this._getCommunicationMapper().mapFromAPI(response.data);
this.logger.info('Repository: internal communication created successfully', {
ticketNumber,
communicationId: createdCommunication.id,
hasAttachments: !!(createdCommunication.attachments && createdCommunication.attachments.length > 0)
});
return createdCommunication;
} catch (error) {
timer();
this.logger.error('Repository: failed to create internal communication', {
ticketNumber,
error: error.message,
statusCode: error.statusCode
});
if (error instanceof APIError) {
throw error;
}
throw new APIError(
`Falha ao criar comunicação interna: ${error.message}`,
error.statusCode || 500,
{ originalError: error.message }
);
}
}
/**
* Lista comunicações internas de um ticket
*/
async list(ticketNumber, options = {}) {
const timer = this.logger.startTimer(`repo_list_communications_${ticketNumber}`);
try {
this.logger.debug('Repository: listing internal communications', {
ticketNumber,
limit: options.limit,
offset: options.offset
});
// Constrói query parameters
const queryParams = new URLSearchParams();
if (options.limit) {
queryParams.set('limit', Math.min(options.limit, 200).toString());
}
if (options.offset) {
queryParams.set('offset', Math.max(options.offset, 1).toString());
}
const endpoint = `/tickets/${ticketNumber}/internal_communications?${queryParams}`;
const response = await this.httpClient.get(endpoint, {
timeout: 20000,
maxRetries: 2
});
timer();
// Valida resposta da listagem
if (response.statusCode >= 400) {
const errorMessage = this._extractAPIErrorMessage(response.data);
throw new APIError(
`Falha ao listar comunicações internas: ${errorMessage}`,
response.statusCode,
response.data
);
}
// Mapeia lista de comunicações
const communicationList = this._getCommunicationMapper().mapListFromAPI(response.data);
this.logger.info('Repository: internal communications listed successfully', {
ticketNumber,
count: communicationList.communications?.length || 0
});
return communicationList;
} catch (error) {
timer();
this.logger.error('Repository: failed to list internal communications', {
ticketNumber,
error: error.message,
statusCode: error.statusCode
});
if (error instanceof APIError) {
throw error;
}
throw new APIError(
`Falha ao listar comunicações internas: ${error.message}`,
error.statusCode || 500,
{ originalError: error.message }
);
}
}
/**
* Busca uma comunicação interna específica
*/
async getById(ticketNumber, communicationId) {
const timer = this.logger.startTimer(`repo_get_communication_${ticketNumber}_${communicationId}`);
try {
this.logger.debug('Repository: fetching internal communication by ID', {
ticketNumber,
communicationId
});
const response = await this.httpClient.get(
`/tickets/${ticketNumber}/internal_communications/${communicationId}`,
{
timeout: 15000,
maxRetries: 2
}
);
timer();
// Verifica se comunicação foi encontrada
if (response.statusCode === 404) {
return null;
}
// Valida resposta da API
if (response.statusCode >= 400) {
const errorMessage = this._extractAPIErrorMessage(response.data);
throw new APIError(
`Falha ao buscar comunicação interna #${communicationId}: ${errorMessage}`,
response.statusCode,
response.data
);
}
// Mapeia comunicação
const communication = this._getCommunicationMapper().mapFromAPI(response.data);
this.logger.debug('Repository: internal communication fetched successfully', {
ticketNumber,
communicationId,
hasAttachments: !!(communication.attachments && communication.attachments.length > 0)
});
return communication;
} catch (error) {
timer();
this.logger.error('Repository: failed to get internal communication', {
ticketNumber,
communicationId,
error: error.message,
statusCode: error.statusCode
});
if (error instanceof APIError) {
throw error;
}
throw new APIError(
`Falha ao buscar comunicação interna #${communicationId}: ${error.message}`,
error.statusCode || 500,
{ originalError: error.message }
);
}
}
/**
* Verifica se uma comunicação existe
*/
async exists(ticketNumber, communicationId) {
try {
const communication = await this.getById(ticketNumber, communicationId);
return communication !== null;
} catch (error) {
if (error instanceof APIError && error.statusCode === 404) {
return false;
}
throw error;
}
}
/**
* Busca comunicações por critérios (se a API suportar)
*/
async searchByCriteria(ticketNumber, criteria = {}) {
const timer = this.logger.startTimer(`repo_search_communications_${ticketNumber}`);
try {
this.logger.debug('Repository: searching internal communications by criteria', {
ticketNumber,
criteriaKeys: Object.keys(criteria)
});
const queryParams = new URLSearchParams();
// Adiciona critérios de busca suportados
if (criteria.author_id) {
queryParams.set('author_id', criteria.author_id.toString());
}
if (criteria.date_from) {
queryParams.set('date_from', criteria.date_from);
}
if (criteria.date_to) {
queryParams.set('date_to', criteria.date_to);
}
if (criteria.has_attachments !== undefined) {
queryParams.set('has_attachments', criteria.has_attachments.toString());
}
if (criteria.text_search) {
queryParams.set('search', criteria.text_search);
}
const response = await this.httpClient.get(
`/tickets/${ticketNumber}/internal_communications/search?${queryParams}`,
{
timeout: 25000,
maxRetries: 2
}
);
timer();
// Valida resposta
if (response.statusCode >= 400) {
const errorMessage = this._extractAPIErrorMessage(response.data);
throw new APIError(
`Falha na busca de comunicações: ${errorMessage}`,
response.statusCode,
response.data
);
}
// Mapeia resultados
const communications = this._getCommunicationMapper().mapListFromAPI(response.data);
this.logger.info('Repository: communication search completed', {
ticketNumber,
criteriaKeys: Object.keys(criteria),
resultCount: communications.communications?.length || 0
});
return communications;
} catch (error) {
timer();
this.logger.error('Repository: failed to search internal communications', {
ticketNumber,
criteria,
error: error.message,
statusCode: error.statusCode
});
if (error instanceof APIError) {
throw error;
}
throw new APIError(
`Falha na busca de comunicações: ${error.message}`,
error.statusCode || 500,
{ originalError: error.message }
);
}
}
/**
* Extrai mensagem de erro da resposta da API
*/
_extractAPIErrorMessage(errorData) {
if (!errorData) {
return 'Erro desconhecido';
}
if (typeof errorData === 'string') {
return errorData;
}
// Tenta diferentes formatos de erro da API TiFlux
if (errorData.message) {
return errorData.message;
}
if (errorData.error) {
if (typeof errorData.error === 'string') {
return errorData.error;
}
if (errorData.error.message) {
return errorData.error.message;
}
}
if (errorData.errors && Array.isArray(errorData.errors)) {
return errorData.errors.join(', ');
}
if (errorData.details) {
return errorData.details;
}
// Erros específicos de upload
if (errorData.file_errors && Array.isArray(errorData.file_errors)) {
return `Erros de arquivo: ${errorData.file_errors.join(', ')}`;
}
return JSON.stringify(errorData).substring(0, 200);
}
/**
* Lazy loading do CommunicationMapper
*/
_getCommunicationMapper() {
if (!this.communicationMapper) {
const CommunicationMapper = require('./CommunicationMapper');
this.communicationMapper = new CommunicationMapper(this.container);
}
return this.communicationMapper;
}
/**
* Estatísticas do repository
*/
getStats() {
return {
endpoints: {
create: '/tickets/:number/internal_communications',
list: '/tickets/:number/internal_communications',
get_by_id: '/tickets/:number/internal_communications/:id',
search: '/tickets/:number/internal_communications/search'
},
features: {
file_upload: true,
multipart_form_data: true,
pagination: true,
search_by_criteria: true
},
timeouts: {
create: '60s (file upload)',
list: '20s',
get_by_id: '15s',
search: '25s'
},
limits: {
max_files_per_request: this.config.get('communications.maxFiles', 10),
max_file_size: this.config.get('communications.maxFileSize', 25 * 1024 * 1024)
}
};
}
}
// Import das classes de erro
const { APIError } = require('../../utils/errors');
module.exports = CommunicationRepository;