UNPKG

@tiflux/mcp

Version:

TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients

514 lines (420 loc) 13 kB
/** * TicketRepository - Acesso a dados para tickets * * Responsabilidades: * - Abstrai a API do TiFlux para operações de ticket * - Padroniza responses e error handling * - Implementa retry e timeout específicos * - Converte dados da API para formato interno */ class TicketRepository { constructor(container) { this.container = container; this.logger = container.resolve('logger'); this.httpClient = container.resolve('tifluxHttpClient'); this.config = container.resolve('config'); this.ticketMapper = null; // Lazy loading } /** * Busca um ticket por ID */ async getById(ticketId) { const timer = this.logger.startTimer(`repo_get_ticket_${ticketId}`); try { this.logger.debug('Repository: fetching ticket by ID', { ticketId }); const response = await this.httpClient.get(`/tickets/${ticketId}`, { timeout: 15000, // Timeout específico para get ticket maxRetries: 2 }); timer(); // Verifica se ticket foi encontrado if (response.statusCode === 404) { return null; } // Valida resposta da API if (!response.data || response.data.error) { throw new APIError( response.data?.error || 'Erro desconhecido na API', response.statusCode, response.data ); } // Mapeia dados da API para formato interno const mappedTicket = this._getTicketMapper().mapFromAPI(response.data); this.logger.debug('Repository: ticket fetched successfully', { ticketId, hasData: !!mappedTicket }); return mappedTicket; } catch (error) { timer(); this.logger.error('Repository: failed to get ticket', { ticketId, error: error.message, statusCode: error.statusCode }); // Re-throw com contexto adicional if (error instanceof APIError) { throw error; } throw new APIError( `Falha ao buscar ticket #${ticketId}: ${error.message}`, error.statusCode || 500, { originalError: error.message } ); } } /** * Cria um novo ticket */ async create(ticketData) { const timer = this.logger.startTimer('repo_create_ticket'); try { this.logger.debug('Repository: creating ticket', { title: ticketData.title?.substring(0, 50), clientId: ticketData.client_id }); // Mapeia dados internos para formato da API const apiData = this._getTicketMapper().mapToAPI(ticketData); const response = await this.httpClient.post('/tickets', apiData, { timeout: 30000, // Timeout maior para criação maxRetries: 1, // Apenas 1 retry para evitar duplicação headers: { 'Content-Type': 'application/json' } }); timer(); // Valida resposta da criação if (response.statusCode >= 400) { const errorMessage = this._extractAPIErrorMessage(response.data); throw new APIError( `Falha ao criar ticket: ${errorMessage}`, response.statusCode, response.data ); } // Mapeia ticket criado const createdTicket = this._getTicketMapper().mapFromAPI(response.data); this.logger.info('Repository: ticket created successfully', { ticketId: createdTicket.id, title: createdTicket.title?.substring(0, 50) }); return createdTicket; } catch (error) { timer(); this.logger.error('Repository: failed to create ticket', { title: ticketData.title?.substring(0, 50), error: error.message, statusCode: error.statusCode }); if (error instanceof APIError) { throw error; } throw new APIError( `Falha ao criar ticket: ${error.message}`, error.statusCode || 500, { originalError: error.message } ); } } /** * Atualiza um ticket existente */ async update(ticketId, updateData) { const timer = this.logger.startTimer(`repo_update_ticket_${ticketId}`); try { this.logger.debug('Repository: updating ticket', { ticketId, fields: Object.keys(updateData) }); // Mapeia dados de atualização para formato da API const apiData = this._getTicketMapper().mapUpdateToAPI(updateData); this.logger.info('Repository: API payload for update', { ticketId, apiData: JSON.stringify(apiData) }); this.logger.info('Repository: Sending HTTP PUT request', { ticketId, url: `/tickets/${ticketId}`, payload: JSON.stringify(apiData), payloadKeys: Object.keys(apiData) }); const response = await this.httpClient.put(`/tickets/${ticketId}`, apiData, { timeout: 20000, maxRetries: 1, headers: { 'Content-Type': 'application/json' } }); this.logger.info('Repository: Received HTTP response', { ticketId, statusCode: response.statusCode, hasError: response.statusCode >= 400 }); timer(); // Valida resposta da atualização if (response.statusCode >= 400) { const errorMessage = this._extractAPIErrorMessage(response.data); throw new APIError( `Falha ao atualizar ticket #${ticketId}: ${errorMessage}`, response.statusCode, response.data ); } // Mapeia ticket atualizado const updatedTicket = this._getTicketMapper().mapFromAPI(response.data); this.logger.info('Repository: ticket updated successfully', { ticketId, updatedFields: Object.keys(updateData) }); return updatedTicket; } catch (error) { timer(); this.logger.error('Repository: failed to update ticket', { ticketId, error: error.message, statusCode: error.statusCode }); if (error instanceof APIError) { throw error; } throw new APIError( `Falha ao atualizar ticket #${ticketId}: ${error.message}`, error.statusCode || 500, { originalError: error.message } ); } } /** * Fecha um ticket específico */ async close(ticketNumber) { const timer = this.logger.startTimer(`repo_close_ticket_${ticketNumber}`); try { this.logger.debug('Repository: closing ticket', { ticketNumber }); const response = await this.httpClient.put(`/tickets/${ticketNumber}/close`, {}, { timeout: 20000, maxRetries: 1, headers: { 'Content-Type': 'application/json' } }); timer(); // Valida resposta do fechamento if (response.statusCode >= 400) { const errorMessage = this._extractAPIErrorMessage(response.data); throw new APIError( `Falha ao fechar ticket #${ticketNumber}: ${errorMessage}`, response.statusCode, response.data ); } this.logger.info('Repository: ticket closed successfully', { ticketNumber, message: response.data?.message }); return response.data || { message: `Ticket ${ticketNumber} closed successfully` }; } catch (error) { timer(); this.logger.error('Repository: failed to close ticket', { ticketNumber, error: error.message, statusCode: error.statusCode }); if (error instanceof APIError) { throw error; } throw new APIError( `Falha ao fechar ticket #${ticketNumber}: ${error.message}`, error.statusCode || 500, { originalError: error.message } ); } } /** * Lista tickets com filtros */ async list(filters = {}) { const timer = this.logger.startTimer('repo_list_tickets'); try { this.logger.debug('Repository: listing tickets', { filters: Object.keys(filters), limit: filters.limit, offset: filters.offset }); // Constrói query parameters const queryParams = this._buildListQueryParams(filters); const endpoint = `/tickets?${new URLSearchParams(queryParams).toString()}`; const response = await this.httpClient.get(endpoint, { timeout: 25000, // Timeout maior para listas maxRetries: 2 }); timer(); // Valida resposta da listagem if (response.statusCode >= 400) { const errorMessage = this._extractAPIErrorMessage(response.data); throw new APIError( `Falha ao listar tickets: ${errorMessage}`, response.statusCode, response.data ); } // Mapeia lista de tickets const ticketList = this._getTicketMapper().mapListFromAPI(response.data); this.logger.info('Repository: tickets listed successfully', { count: ticketList.tickets?.length || 0, hasMore: !!ticketList.pagination?.has_more }); return ticketList; } catch (error) { timer(); this.logger.error('Repository: failed to list tickets', { filters: Object.keys(filters), error: error.message, statusCode: error.statusCode }); if (error instanceof APIError) { throw error; } throw new APIError( `Falha ao listar tickets: ${error.message}`, error.statusCode || 500, { originalError: error.message } ); } } /** * Verifica se um ticket existe */ async exists(ticketId) { try { const ticket = await this.getById(ticketId); return ticket !== null; } catch (error) { if (error instanceof APIError && error.statusCode === 404) { return false; } throw error; } } /** * Constrói query parameters para listagem */ _buildListQueryParams(filters) { const params = {}; // Filtros obrigatórios if (filters.desk_ids) { params.desk_ids = filters.desk_ids; } if (filters.client_ids) { params.client_ids = filters.client_ids; } if (filters.stage_ids) { params.stage_ids = filters.stage_ids; } if (filters.responsible_ids) { params.responsible_ids = filters.responsible_ids; } // Filtros opcionais if (typeof filters.is_closed !== 'undefined') { params.is_closed = filters.is_closed; } if (filters.limit) { params.limit = Math.min(filters.limit, 200); } if (filters.offset) { params.offset = Math.max(filters.offset, 1); } // Filtros por nome (se suportados pela API) if (filters.desk_name) { params.desk_name = filters.desk_name; } if (filters.stage_name) { params.stage_name = filters.stage_name; } return params; } /** * 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; } return JSON.stringify(errorData).substring(0, 200); } /** * Lazy loading do TicketMapper */ _getTicketMapper() { if (!this.ticketMapper) { this.ticketMapper = this.container.resolve('ticketMapper'); } return this.ticketMapper; } /** * Busca arquivos anexados a um ticket */ async getFiles(ticketNumber) { const timer = this.logger.startTimer(`repository_get_ticket_files_${ticketNumber}`); try { this.logger.debug('Fetching ticket files from API', { ticketNumber }); const response = await this._getHttpClient().get(`/tickets/${ticketNumber}/files`); timer(); if (!response || !response.data) { this.logger.warn('No files data in response', { ticketNumber }); return []; } return response.data; } catch (error) { timer(); this.logger.error('Failed to fetch ticket files', { ticketNumber, error: error.message, statusCode: error.statusCode }); throw new APIError( `Falha ao buscar arquivos do ticket #${ticketNumber}: ${error.message}`, error.statusCode ); } } /** * Estatísticas do repository */ getStats() { return { httpClient: { configured: !!this.httpClient, baseUrl: this.config.get('api.url') }, mapper: { loaded: !!this.ticketMapper } }; } } // Import das classes de erro const { APIError } = require('../../utils/errors'); module.exports = TicketRepository;