@tiflux/mcp
Version:
TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients
544 lines (458 loc) • 14.8 kB
JavaScript
/**
* TicketMapper - Transformação de dados entre API e domínio
*
* Responsabilidades:
* - Converter dados da API TiFlux para formato interno
* - Converter dados internos para formato da API
* - Normalizar campos e estruturas
* - Garantir compatibilidade entre versões
* - Aplicar transformações específicas
*/
class TicketMapper {
constructor(container) {
this.container = container;
this.logger = container.resolve('logger');
this.config = container.resolve('config');
}
/**
* Mapeia dados da API para formato interno
*/
mapFromAPI(apiData) {
if (!apiData) {
return null;
}
try {
// A API pode retornar o ticket dentro de { ticket: {...} } ou direto
const ticketData = apiData.ticket || apiData;
const mapped = {
// IDs e identificadores
id: this._extractId(ticketData),
number: ticketData.number || ticketData.id,
// Campos básicos
title: this._extractString(ticketData.title),
description: this._extractString(ticketData.description),
// Status e estado
status: this._mapStatus(ticketData.status),
priority: this._mapPriority(ticketData.priority),
stage: this._mapStage(ticketData.stage),
// Relacionamentos
client: this._mapClient(ticketData.client),
desk: this._mapDesk(ticketData.desk),
assigned_to: this._mapAssignedUser(ticketData.assigned_to || ticketData.responsible),
responsible: this._mapAssignedUser(ticketData.responsible || ticketData.assigned_to),
// Solicitante
requestor: this._mapRequestor(ticketData),
// Seguidores
followers: this._mapFollowers(ticketData.followers),
// Datas
created_at: this._mapDateTime(ticketData.created_at),
updated_at: this._mapDateTime(ticketData.updated_at),
due_date: this._mapDateTime(ticketData.due_date),
closed_at: this._mapDateTime(ticketData.closed_at),
// Campos computados
is_closed: this._isTicketClosed(ticketData),
is_overdue: this._isTicketOverdue(ticketData),
// Metadados
url: this._buildTicketURL(ticketData.id || ticketData.number),
api_version: 'v2'
};
// Remove campos null/undefined
return this._cleanObject(mapped);
} catch (error) {
this.logger.error('Failed to map ticket from API', {
error: error.message,
ticketId: apiData.id || apiData.ticket?.id
});
throw new Error(`Falha ao mapear dados do ticket: ${error.message}`);
}
}
/**
* Mapeia dados internos para formato da API (criação)
*/
mapToAPI(internalData) {
if (!internalData) {
return null;
}
try {
const apiData = {
// Campos obrigatórios
title: internalData.title,
description: internalData.description,
// Cliente
client_id: this._extractId(internalData.client_id),
// Campos opcionais com defaults
desk_id: this._extractId(internalData.desk_id) || this.config.get('defaults.desk_id'),
priority_id: this._extractId(internalData.priority_id) || this.config.get('defaults.priority_id'),
services_catalogs_item_id: this._extractId(internalData.services_catalogs_item_id) || this.config.get('defaults.catalog_item_id'),
// Status inicial
status_id: this._extractId(internalData.status_id) || this.config.get('defaults.status_id'),
// Responsável (opcional)
responsible_id: this._extractId(internalData.responsible_id),
// Dados do solicitante
requestor_name: this._extractString(internalData.requestor_name),
requestor_email: this._extractString(internalData.requestor_email),
requestor_telephone: this._extractString(internalData.requestor_telephone),
// Seguidores
followers: this._extractString(internalData.followers)
};
// Remove campos null/undefined
return this._cleanObject(apiData);
} catch (error) {
this.logger.error('Failed to map ticket to API', {
error: error.message,
title: internalData.title
});
throw new Error(`Falha ao preparar dados para API: ${error.message}`);
}
}
/**
* Mapeia dados de atualização para formato da API
*/
mapUpdateToAPI(updateData) {
if (!updateData || Object.keys(updateData).length === 0) {
return {};
}
try {
const apiData = {};
// Campos que podem ser atualizados diretamente
if (updateData.title !== undefined) {
apiData.title = updateData.title;
}
if (updateData.description !== undefined) {
apiData.description = updateData.description;
}
// IDs que podem ser atualizados
const idFields = [
'client_id', 'desk_id', 'priority_id', 'status_id',
'stage_id', 'responsible_id', 'services_catalogs_item_id'
];
idFields.forEach(field => {
if (updateData[field] !== undefined) {
if (updateData[field] === null) {
apiData[field] = null;
} else {
apiData[field] = this._extractId(updateData[field]);
}
}
});
// Seguidores
if (updateData.followers !== undefined) {
apiData.followers = updateData.followers === null ? null : this._extractString(updateData.followers);
}
// Dados do solicitante (requestor)
if (updateData.requestor_name !== undefined) {
apiData.requestor_name = this._extractString(updateData.requestor_name);
}
if (updateData.requestor_email !== undefined) {
apiData.requestor_email = this._extractString(updateData.requestor_email);
}
if (updateData.requestor_telephone !== undefined) {
apiData.requestor_telephone = this._extractString(updateData.requestor_telephone);
}
return this._cleanObject(apiData);
} catch (error) {
this.logger.error('Failed to map ticket update to API', {
error: error.message,
fields: Object.keys(updateData)
});
throw new Error(`Falha ao preparar dados de atualização: ${error.message}`);
}
}
/**
* Mapeia lista de tickets da API
*/
mapListFromAPI(apiResponse) {
try {
// A API pode retornar diferentes formatos de lista
let tickets = [];
let pagination = null;
if (Array.isArray(apiResponse)) {
// Array simples de tickets
tickets = apiResponse;
} else if (apiResponse.tickets && Array.isArray(apiResponse.tickets)) {
// Formato { tickets: [...], pagination: {...} }
tickets = apiResponse.tickets;
pagination = apiResponse.pagination || apiResponse.meta;
} else if (apiResponse.data && Array.isArray(apiResponse.data)) {
// Formato { data: [...], meta: {...} }
tickets = apiResponse.data;
pagination = apiResponse.meta || apiResponse.pagination;
}
// Mapeia cada ticket
const mappedTickets = tickets.map(ticket => {
try {
return this.mapFromAPI(ticket);
} catch (error) {
this.logger.warn('Failed to map individual ticket in list', {
ticketId: ticket.id,
error: error.message
});
// Retorna ticket com dados básicos em caso de erro
return {
id: ticket.id,
title: ticket.title || 'Erro ao carregar',
status: { name: 'Erro' },
client: { name: 'N/A' }
};
}
});
return {
tickets: mappedTickets,
pagination: this._mapPagination(pagination),
total_count: mappedTickets.length
};
} catch (error) {
this.logger.error('Failed to map ticket list from API', {
error: error.message
});
throw new Error(`Falha ao mapear lista de tickets: ${error.message}`);
}
}
/**
* Mapeia objeto de status
*/
_mapStatus(status) {
if (!status) return null;
if (typeof status === 'string') {
return { name: status };
}
return {
id: this._extractId(status.id),
name: this._extractString(status.name) || 'N/A',
color: this._extractString(status.color),
is_closed: !!status.is_closed
};
}
/**
* Mapeia objeto de prioridade
*/
_mapPriority(priority) {
if (!priority) return null;
if (typeof priority === 'string') {
return { name: priority };
}
return {
id: this._extractId(priority.id),
name: this._extractString(priority.name) || 'N/A',
level: this._extractNumber(priority.level),
color: this._extractString(priority.color)
};
}
/**
* Mapeia objeto de estágio
*/
_mapStage(stage) {
if (!stage) return null;
if (typeof stage === 'string') {
return { name: stage };
}
return {
id: this._extractId(stage.id),
name: this._extractString(stage.name) || 'N/A',
order: this._extractNumber(stage.order)
};
}
/**
* Mapeia objeto de cliente
*/
_mapClient(client) {
if (!client) return null;
if (typeof client === 'string') {
return { name: client };
}
return {
id: this._extractId(client.id),
name: this._extractString(client.name) || 'Cliente N/A',
email: this._extractString(client.email),
phone: this._extractString(client.phone)
};
}
/**
* Mapeia objeto de mesa/departamento
*/
_mapDesk(desk) {
if (!desk) return null;
if (typeof desk === 'string') {
return { name: desk };
}
return {
id: this._extractId(desk.id),
name: this._extractString(desk.name) || 'Mesa N/A',
department: this._extractString(desk.department)
};
}
/**
* Mapeia usuário responsável/atribuído
*/
_mapAssignedUser(user) {
if (!user) return null;
if (typeof user === 'string') {
return { name: user };
}
return {
id: this._extractId(user.id),
name: this._extractString(user.name) || 'Usuário N/A',
email: this._extractString(user.email),
role: this._extractString(user.role)
};
}
/**
* Mapeia dados do solicitante
*/
_mapRequestor(ticketData) {
const requestor = {
name: this._extractString(ticketData.requestor_name || ticketData.requester_name),
email: this._extractString(ticketData.requestor_email || ticketData.requester_email),
phone: this._extractString(ticketData.requestor_telephone || ticketData.requester_phone)
};
// Retorna null se não há dados de solicitante
if (!requestor.name && !requestor.email && !requestor.phone) {
return null;
}
return requestor;
}
/**
* Mapeia seguidores
*/
_mapFollowers(followersData) {
if (!followersData) return [];
if (typeof followersData === 'string') {
return followersData.split(',').map(email => email.trim()).filter(email => email);
}
if (Array.isArray(followersData)) {
return followersData.map(follower => {
if (typeof follower === 'string') return follower.trim();
return follower.email || follower.name || 'N/A';
}).filter(email => email);
}
return [];
}
/**
* Mapeia paginação
*/
_mapPagination(paginationData) {
if (!paginationData) return null;
return {
current_page: this._extractNumber(paginationData.current_page || paginationData.page) || 1,
total_pages: this._extractNumber(paginationData.total_pages || paginationData.pages),
per_page: this._extractNumber(paginationData.per_page || paginationData.limit) || 20,
total_count: this._extractNumber(paginationData.total_count || paginationData.total),
has_more: !!paginationData.has_more || !!paginationData.next_page
};
}
/**
* Mapeia data/hora
*/
_mapDateTime(dateTime) {
if (!dateTime) return null;
try {
const date = new Date(dateTime);
if (isNaN(date.getTime())) return null;
return date.toISOString();
} catch {
return null;
}
}
/**
* Determina se ticket está fechado
*/
_isTicketClosed(ticketData) {
// Verifica pelo status
if (ticketData.status && ticketData.status.is_closed) {
return true;
}
// Verifica pela data de fechamento
if (ticketData.closed_at) {
return true;
}
// Verifica por nomes de status conhecidos
const statusName = (ticketData.status?.name || ticketData.status || '').toLowerCase();
const closedStatuses = ['fechado', 'closed', 'resolvido', 'resolved', 'cancelado', 'cancelled'];
return closedStatuses.some(status => statusName.includes(status));
}
/**
* Determina se ticket está atrasado
*/
_isTicketOverdue(ticketData) {
if (!ticketData.due_date) return false;
if (this._isTicketClosed(ticketData)) return false;
try {
const dueDate = new Date(ticketData.due_date);
const now = new Date();
return dueDate < now;
} catch {
return false;
}
}
/**
* Constrói URL do ticket
*/
_buildTicketURL(ticketId) {
if (!ticketId) return null;
const baseUrl = this.config.get('ui.base_url', 'https://app.tiflux.com');
return `${baseUrl}/tickets/${ticketId}`;
}
/**
* Extrai ID numérico
*/
_extractId(value) {
if (value === null || value === undefined) return null;
const id = parseInt(value);
return isNaN(id) ? null : id;
}
/**
* Extrai string
*/
_extractString(value) {
if (value === null || value === undefined) return null;
return String(value).trim() || null;
}
/**
* Extrai número
*/
_extractNumber(value) {
if (value === null || value === undefined) return null;
const num = parseFloat(value);
return isNaN(num) ? null : num;
}
/**
* Remove campos null/undefined de um objeto
*/
_cleanObject(obj) {
const cleaned = {};
Object.keys(obj).forEach(key => {
const value = obj[key];
if (value !== null && value !== undefined) {
if (typeof value === 'object' && !Array.isArray(value)) {
const cleanedChild = this._cleanObject(value);
if (Object.keys(cleanedChild).length > 0) {
cleaned[key] = cleanedChild;
}
} else {
cleaned[key] = value;
}
}
});
return cleaned;
}
/**
* Estatísticas do mapper
*/
getStats() {
return {
supported_formats: {
api_to_internal: true,
internal_to_api: true,
list_mapping: true,
update_mapping: true
},
transformations: {
status_normalization: true,
date_iso_conversion: true,
id_extraction: true,
url_generation: true
}
};
}
}
module.exports = TicketMapper;