@ai-growth/n8n-nodes-wordpress
Version:
n8n node for WordPress integration with AI GROWTH - SEO WP plugin
652 lines • 29.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WordPressClient = void 0;
const axios_1 = __importDefault(require("axios"));
const form_data_1 = __importDefault(require("form-data"));
const Validator_1 = require("./Validator");
const ErrorUtils_1 = require("./ErrorUtils");
const Logger_1 = require("./Logger");
const AuthHeaderManager_1 = require("./AuthHeaderManager");
/**
* Cliente HTTP para a API REST do WordPress
* Encapsula o Axios e fornece métodos para interagir com a API
*/
class WordPressClient {
/**
* Construtor do cliente WordPress
* @param credentials Credenciais para acesso à API
* @param options Opções de configuração
*/
constructor(credentials, options = {}) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
this.discoveredRoutes = [];
this.routeDiscoveryCache = new Map();
this.apiVersionDetected = false;
// Validar e sanitizar URL
const validationResult = Validator_1.Validator.validateCredentials(credentials);
if (!validationResult.valid) {
throw new ErrorUtils_1.WordPressError(`Invalid WordPress credentials: ${validationResult.error}`, ErrorUtils_1.WordPressErrorType.VALIDATION);
}
this.credentials = {
...credentials,
url: Validator_1.Validator.sanitizeUrl(credentials.url),
};
this.baseUrl = this.credentials.url;
// Definir opções padrão
this.options = {
timeout: (_a = options.timeout) !== null && _a !== void 0 ? _a : 30000,
maxRetries: (_b = options.maxRetries) !== null && _b !== void 0 ? _b : 3,
retryDelay: (_c = options.retryDelay) !== null && _c !== void 0 ? _c : 1000,
debug: (_d = options.debug) !== null && _d !== void 0 ? _d : false,
apiVersion: (_e = options.apiVersion) !== null && _e !== void 0 ? _e : 'v2',
logOptions: (_f = options.logOptions) !== null && _f !== void 0 ? _f : {},
};
// Configurar o logger
this.logger = new Logger_1.Logger({
...this.options.logOptions,
prefix: (_h = (_g = this.options.logOptions) === null || _g === void 0 ? void 0 : _g.prefix) !== null && _h !== void 0 ? _h : '[WordPress API]',
level: this.options.debug
? Logger_1.LogLevel.DEBUG
: ((_k = (_j = this.options.logOptions) === null || _j === void 0 ? void 0 : _j.level) !== null && _k !== void 0 ? _k : Logger_1.LogLevel.ERROR),
});
// Configurar o gerenciador de autenticação
this.authManager = new AuthHeaderManager_1.AuthHeaderManager(this.credentials);
// Criar instância do Axios
this.client = this.createAxiosInstance();
// Configurar interceptadores
this.setupInterceptors();
// Logar inicialização
this.logger.info(`WordPress client initialized for ${this.baseUrl}/wp-json/wp/${this.options.apiVersion}`);
this.logger.debug('Client configuration:', {
baseUrl: this.baseUrl,
apiVersion: this.options.apiVersion,
timeout: this.options.timeout,
maxRetries: this.options.maxRetries,
retryDelay: this.options.retryDelay,
debug: this.options.debug,
});
}
/**
* Cria a instância do Axios com as configurações necessárias
* @returns Instância configurada do Axios
*/
createAxiosInstance() {
// Obter cabeçalhos básicos de autenticação
const headers = this.authManager.getBasicHeaders();
// Criar e retornar a instância
return axios_1.default.create({
baseURL: `${this.baseUrl}/wp-json/wp/${this.options.apiVersion}`,
headers,
timeout: this.options.timeout,
});
}
/**
* Configura interceptadores para request/response
*/
setupInterceptors() {
// Interceptador de requisição
this.client.interceptors.request.use((config) => {
var _a;
const method = ((_a = config.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'UNKNOWN';
const url = config.url || 'UNKNOWN';
const fullUrl = config.baseURL ? `${config.baseURL}/${url}` : url;
this.logger.request(method, fullUrl, config.params, config.data);
// Log detalhado dos headers (sem credenciais)
if (this.logger.isLevelEnabled(Logger_1.LogLevel.TRACE)) {
const safeHeaders = { ...config.headers };
if (safeHeaders.Authorization) {
safeHeaders.Authorization = 'Basic ***REDACTED***';
}
this.logger.trace('Request headers:', safeHeaders);
}
return config;
}, (error) => {
this.logger.error('Request configuration error', error);
return Promise.reject(error);
});
// Interceptador de resposta
this.client.interceptors.response.use((response) => {
var _a;
const method = ((_a = response.config.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'UNKNOWN';
const url = response.config.url || 'UNKNOWN';
const fullUrl = response.config.baseURL ? `${response.config.baseURL}/${url}` : url;
this.logger.response(method, fullUrl, response.status, response.data);
// Log detalhado dos headers de resposta
if (this.logger.isLevelEnabled(Logger_1.LogLevel.TRACE)) {
this.logger.trace('Response headers:', response.headers);
}
return response;
}, (error) => {
var _a, _b, _c, _d, _e, _f;
// Logar o erro
const method = ((_b = (_a = error.config) === null || _a === void 0 ? void 0 : _a.method) === null || _b === void 0 ? void 0 : _b.toUpperCase()) || 'UNKNOWN';
const url = ((_c = error.config) === null || _c === void 0 ? void 0 : _c.url) || 'UNKNOWN';
const fullUrl = ((_d = error.config) === null || _d === void 0 ? void 0 : _d.baseURL) ? `${error.config.baseURL}/${url}` : url;
this.logger.httpError(method, fullUrl, error);
// Log adicional para diagnóstico
if (error.response) {
this.logger.error('HTTP Error Response Details:', {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
});
}
else if (error.request) {
this.logger.error('HTTP Request Error Details:', {
timeout: (_e = error.config) === null || _e === void 0 ? void 0 : _e.timeout,
method: (_f = error.config) === null || _f === void 0 ? void 0 : _f.method,
url: fullUrl,
code: error.code,
});
}
// Converter para WordPressError
const wpError = ErrorUtils_1.ErrorUtils.fromError(error);
return Promise.reject(wpError);
});
}
/**
* Executa uma requisição com suporte a retry
* @param requestFn Função que executa a requisição
* @returns Resultado da requisição
*/
async executeWithRetry(requestFn) {
let lastError = null;
let attempt = 0;
this.logger.debug(`Starting request with retry (max attempts: ${this.options.maxRetries})`);
while (attempt < this.options.maxRetries) {
try {
attempt++;
this.logger.debug(`Attempt ${attempt}/${this.options.maxRetries}`);
const startTime = Date.now();
const response = await requestFn();
const duration = Date.now() - startTime;
this.logger.debug(`Request completed successfully in ${duration}ms`);
return response.data;
}
catch (error) {
// Converter para WordPressError se ainda não for
const wpError = error instanceof ErrorUtils_1.WordPressError
? error
: ErrorUtils_1.ErrorUtils.fromError(error);
lastError = wpError;
this.logger.retry(attempt, this.options.maxRetries, wpError);
// Verificar se o erro é retentável
if (!wpError.isRetryable()) {
this.logger.warn(`Error not retryable (${wpError.type}): ${wpError.message}`);
break;
}
// Última tentativa, não esperar
if (attempt >= this.options.maxRetries) {
this.logger.warn(`Maximum retry attempts (${this.options.maxRetries}) reached`);
break;
}
// Calcular tempo de espera com backoff exponencial
const delayTime = this.calculateBackoff(attempt);
// Esperar antes da próxima tentativa
this.logger.retry(attempt, this.options.maxRetries, wpError, delayTime);
await new Promise(resolve => setTimeout(resolve, delayTime));
}
}
// Todas as tentativas falharam
this.logger.error(`All ${this.options.maxRetries} retry attempts failed`);
throw lastError || new ErrorUtils_1.WordPressError('Request failed after maximum retries', ErrorUtils_1.WordPressErrorType.UNKNOWN);
}
/**
* Calcula o tempo de espera para o backoff exponencial
* @param attempt Número da tentativa atual (começando em 1)
* @returns Tempo de espera em ms
*/
calculateBackoff(attempt) {
// Backoff exponencial com jitter para evitar thundering herd
const baseDelay = this.options.retryDelay;
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * baseDelay;
return Math.min(exponentialDelay + jitter, 30000); // Máximo de 30 segundos
}
/**
* Descobre as rotas disponíveis na API REST do WordPress
* @param forceRefresh Força uma nova descoberta ignorando o cache
* @returns Resultado da descoberta de rotas
*/
async discoverRoutes(forceRefresh = false) {
const cacheKey = `${this.baseUrl}_${this.options.apiVersion}`;
// Verificar cache se não for para forçar refresh
if (!forceRefresh && this.routeDiscoveryCache.has(cacheKey)) {
const cached = this.routeDiscoveryCache.get(cacheKey);
this.logger.debug('Using cached route discovery result');
return cached;
}
this.logger.info('Discovering WordPress REST API routes...');
try {
// Primeiro, tentar descobrir as versões disponíveis da API
const rootResponse = await axios_1.default.get(`${this.baseUrl}/wp-json`, {
timeout: this.options.timeout,
headers: {
'User-Agent': 'n8n-wordpress-node/1.0',
'Accept': 'application/json',
},
});
const availableVersions = [];
const routes = [];
// Extrair informações sobre namespaces disponíveis
if (rootResponse.data && rootResponse.data.namespaces) {
for (const namespace of rootResponse.data.namespaces) {
if (namespace.startsWith('wp/v')) {
const version = namespace.replace('wp/', '');
availableVersions.push(version);
}
}
}
// Se não encontrou versões, assumir v2 como padrão
if (availableVersions.length === 0) {
availableVersions.push('v2');
}
// Descobrir rotas para a versão atual
const currentApiUrl = `${this.baseUrl}/wp-json/wp/${this.options.apiVersion}`;
try {
const apiResponse = await axios_1.default.get(currentApiUrl, {
timeout: this.options.timeout,
headers: {
'User-Agent': 'n8n-wordpress-node/1.0',
'Accept': 'application/json',
},
});
// Processar rotas descobertas
if (apiResponse.data && apiResponse.data.routes) {
for (const [endpoint, routeData] of Object.entries(apiResponse.data.routes)) {
if (typeof routeData === 'object' && routeData !== null) {
const methods = Array.isArray(routeData.methods)
? routeData.methods
: [];
routes.push({
endpoint: endpoint.replace(/^\/wp\/v\d+\//, ''),
methods,
namespace: `wp/${this.options.apiVersion}`,
version: this.options.apiVersion,
});
}
}
}
const result = {
success: true,
routes,
apiVersion: this.options.apiVersion,
availableVersions,
};
// Armazenar no cache
this.routeDiscoveryCache.set(cacheKey, result);
this.discoveredRoutes = routes;
this.logger.info(`Route discovery completed: ${routes.length} routes found for API ${this.options.apiVersion}`);
this.logger.debug('Available API versions:', availableVersions);
return result;
}
catch (apiError) {
this.logger.warn(`Failed to discover routes for API ${this.options.apiVersion}:`, apiError);
const result = {
success: false,
routes: [],
apiVersion: this.options.apiVersion,
availableVersions,
error: `Failed to access API ${this.options.apiVersion}: ${apiError.message}`,
};
return result;
}
}
catch (error) {
this.logger.error('Route discovery failed:', error);
const result = {
success: false,
routes: [],
apiVersion: this.options.apiVersion,
availableVersions: ['v2'], // Fallback padrão
error: `Route discovery failed: ${error.message}`,
};
return result;
}
}
/**
* Detecta automaticamente a versão da API mais adequada
* @returns Resultado da detecção de versão
*/
async detectApiVersion() {
this.logger.info('Detecting optimal WordPress REST API version...');
try {
// Primeiro, descobrir versões disponíveis
const rootResponse = await axios_1.default.get(`${this.baseUrl}/wp-json`, {
timeout: this.options.timeout,
headers: {
'User-Agent': 'n8n-wordpress-node/1.0',
'Accept': 'application/json',
},
});
const availableVersions = [];
if (rootResponse.data && rootResponse.data.namespaces) {
for (const namespace of rootResponse.data.namespaces) {
if (namespace.startsWith('wp/v')) {
const version = namespace.replace('wp/', '');
availableVersions.push(version);
}
}
}
// Se não encontrou versões, assumir v2
if (availableVersions.length === 0) {
availableVersions.push('v2');
}
// Ordenar versões (v2, v3, etc.)
availableVersions.sort((a, b) => {
const aNum = parseInt(a.replace('v', ''));
const bNum = parseInt(b.replace('v', ''));
return bNum - aNum; // Ordem decrescente (mais recente primeiro)
});
// Testar versões em ordem de preferência
for (const version of availableVersions) {
try {
const testUrl = `${this.baseUrl}/wp-json/wp/${version}`;
const testResponse = await axios_1.default.get(testUrl, {
timeout: 10000,
headers: {
'User-Agent': 'n8n-wordpress-node/1.0',
'Accept': 'application/json',
},
});
if (testResponse.data && testResponse.data.routes) {
this.logger.info(`API version ${version} is working and has routes available`);
const result = {
detectedVersion: version,
availableVersions,
isSupported: true,
recommendedVersion: version,
};
this.apiVersionDetected = true;
return result;
}
}
catch (versionError) {
this.logger.debug(`API version ${version} test failed:`, versionError);
continue;
}
}
// Se nenhuma versão funcionou, retornar v2 como fallback
const fallbackVersion = 'v2';
this.logger.warn(`No working API version found, falling back to ${fallbackVersion}`);
return {
detectedVersion: fallbackVersion,
availableVersions,
isSupported: false,
recommendedVersion: availableVersions[0] || fallbackVersion,
};
}
catch (error) {
this.logger.error('API version detection failed:', error);
return {
detectedVersion: 'v2',
availableVersions: ['v2'],
isSupported: false,
};
}
}
/**
* Valida se um endpoint específico está disponível
* @param endpoint Endpoint a ser validado (sem barra inicial)
* @param method Método HTTP (opcional, padrão: GET)
* @returns True se o endpoint estiver disponível
*/
async validateEndpoint(endpoint, method = 'GET') {
// Se ainda não descobriu as rotas, fazer a descoberta
if (this.discoveredRoutes.length === 0) {
const discovery = await this.discoverRoutes();
if (!discovery.success) {
this.logger.warn('Route discovery failed, cannot validate endpoint');
return false;
}
}
// Normalizar endpoint
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
// Procurar por correspondência exata
const exactMatch = this.discoveredRoutes.find(route => route.endpoint === normalizedEndpoint &&
route.methods.includes(method.toUpperCase()));
if (exactMatch) {
this.logger.debug(`Endpoint validation successful: ${method} ${normalizedEndpoint}`);
return true;
}
// Procurar por correspondência de padrão (para endpoints com parâmetros)
const patternMatch = this.discoveredRoutes.find(route => {
// Converter padrões como /posts/(?P<id>[\d]+) para regex
const pattern = route.endpoint
.replace(/\(\?\P<[^>]+>[^)]+\)/g, '[^/]+') // Substituir grupos nomeados
.replace(/\(\?\:[^)]+\)/g, '[^/]+') // Substituir grupos não-capturantes
.replace(/\$/, ''); // Remover âncora de fim
const regex = new RegExp(`^${pattern}$`);
return regex.test(normalizedEndpoint) && route.methods.includes(method.toUpperCase());
});
if (patternMatch) {
this.logger.debug(`Endpoint validation successful (pattern match): ${method} ${normalizedEndpoint}`);
return true;
}
this.logger.debug(`Endpoint validation failed: ${method} ${normalizedEndpoint}`);
return false;
}
/**
* Realiza uma requisição GET
* @param endpoint Endpoint da API (sem barra inicial)
* @param params Parâmetros da query string
* @returns Dados da resposta
*/
async get(endpoint, params) {
const formattedEndpoint = this.formatEndpoint(endpoint);
const fullUrl = `${this.baseUrl}/wp-json/wp/${this.options.apiVersion}/${formattedEndpoint}`;
this.logger.info(`GET ${fullUrl}`);
if (params && Object.keys(params).length > 0) {
this.logger.debug('GET params:', params);
}
try {
const result = await this.executeWithRetry(() => this.client.get(formattedEndpoint, { params }));
this.logger.info(`GET ${fullUrl} completed successfully`);
return result;
}
catch (error) {
// Log detalhado para erros 404 (Resource not found)
if (error && typeof error === 'object' && 'statusCode' in error && error.statusCode === 404) {
this.logger.error(`GET ${fullUrl} failed - Resource not found (404)`, {
endpoint: formattedEndpoint,
params,
fullUrl,
error: error,
});
}
else {
this.logger.error(`GET ${fullUrl} failed`, error);
}
throw error;
}
}
/**
* Realiza uma requisição POST
* @param endpoint Endpoint da API (sem barra inicial)
* @param data Dados a serem enviados
* @returns Dados da resposta
*/
async post(endpoint, data) {
const formattedEndpoint = this.formatEndpoint(endpoint);
const fullUrl = `${this.baseUrl}/wp-json/wp/${this.options.apiVersion}/${formattedEndpoint}`;
this.logger.info(`POST ${fullUrl}`);
this.logger.debug('POST data:', data);
try {
const result = await this.executeWithRetry(() => this.client.post(formattedEndpoint, data));
this.logger.info(`POST ${fullUrl} completed successfully`);
return result;
}
catch (error) {
this.logger.error(`POST ${fullUrl} failed`, error);
throw error;
}
}
/**
* Realiza uma requisição POST com opções adicionais
* @param endpoint Endpoint da API (sem barra inicial)
* @param data Dados a serem enviados
* @param options Opções adicionais da requisição
* @returns Dados da resposta
*/
async postWithOptions(endpoint, data, options) {
const formattedEndpoint = this.formatEndpoint(endpoint);
const fullUrl = `${this.baseUrl}/wp-json/wp/${this.options.apiVersion}/${formattedEndpoint}`;
this.logger.info(`POST ${fullUrl} with options`);
this.logger.debug('POST data:', data);
this.logger.debug('POST options:', options);
try {
const result = await this.executeWithRetry(() => this.client.post(formattedEndpoint, data, options));
this.logger.info(`POST ${fullUrl} with options completed successfully`);
return result;
}
catch (error) {
this.logger.error(`POST ${fullUrl} with options failed`, error);
throw error;
}
}
/**
* Realiza uma requisição PUT
* @param endpoint Endpoint da API (sem barra inicial)
* @param data Dados a serem enviados
* @returns Dados da resposta
*/
async put(endpoint, data) {
const formattedEndpoint = this.formatEndpoint(endpoint);
this.logger.info(`PUT ${formattedEndpoint}`, { data });
try {
return await this.executeWithRetry(() => this.client.put(formattedEndpoint, data));
}
catch (error) {
this.logger.error(`PUT ${formattedEndpoint} failed`, error);
throw error;
}
}
/**
* Realiza uma requisição DELETE
* @param endpoint Endpoint da API (sem barra inicial)
* @returns Dados da resposta
*/
async delete(endpoint) {
const formattedEndpoint = this.formatEndpoint(endpoint);
this.logger.info(`DELETE ${formattedEndpoint}`);
try {
return await this.executeWithRetry(() => this.client.delete(formattedEndpoint));
}
catch (error) {
this.logger.error(`DELETE ${formattedEndpoint} failed`, error);
throw error;
}
}
/**
* Faz upload de um arquivo de mídia para o WordPress
* @param fileData Buffer com os dados do arquivo
* @param fileName Nome do arquivo
* @param mimeType Tipo MIME do arquivo
* @returns Informações do arquivo enviado
*/
async uploadMedia(fileData, fileName, mimeType) {
const uploadUrl = `${this.baseUrl}/wp-json/wp/${this.options.apiVersion}/media`;
this.logger.info(`UPLOAD media: ${fileName} (${mimeType}), size: ${fileData.length} bytes to ${uploadUrl}`);
try {
// Criar form data
const formData = new form_data_1.default();
formData.append('file', fileData, {
filename: fileName,
contentType: mimeType,
});
this.logger.debug('FormData created for upload:', {
fileName,
mimeType,
fileSize: fileData.length,
});
// Obter cabeçalhos para upload de mídia
const headers = this.authManager.getMediaUploadHeaders(formData.getHeaders());
this.logger.debug('Upload headers prepared (auth redacted)');
const result = await this.executeWithRetry(() => {
return axios_1.default.post(uploadUrl, formData, { headers, timeout: this.options.timeout });
});
this.logger.info(`UPLOAD media ${fileName} completed successfully`);
return result;
}
catch (error) {
this.logger.error(`UPLOAD media ${fileName} failed`, error);
throw error;
}
}
/**
* Formata o endpoint removendo barras iniciais e duplicadas
* @param endpoint Endpoint a ser formatado
* @returns Endpoint formatado
*/
formatEndpoint(endpoint) {
return endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
}
/**
* Obter URL base da API
* @returns URL base
*/
getBaseUrl() {
return this.baseUrl;
}
/**
* Obter versão da API
* @returns Versão da API
*/
getApiVersion() {
return this.options.apiVersion;
}
/**
* Definir nível de log
* @param level Nível de log
*/
setLogLevel(level) {
this.logger.setLevel(level);
}
/**
* Atualiza as credenciais do cliente
* @param credentials Novas credenciais
*/
updateCredentials(credentials) {
// Validar novas credenciais
const validationResult = Validator_1.Validator.validateCredentials(credentials);
if (!validationResult.valid) {
throw new ErrorUtils_1.WordPressError(`Invalid WordPress credentials: ${validationResult.error}`, ErrorUtils_1.WordPressErrorType.VALIDATION);
}
// Atualizar credenciais e URL
this.credentials = {
...credentials,
url: Validator_1.Validator.sanitizeUrl(credentials.url),
};
this.baseUrl = this.credentials.url;
// Atualizar gerenciador de autenticação
this.authManager.updateCredentials(this.credentials);
// Recriar cliente
this.client = this.createAxiosInstance();
// Logar atualização
this.logger.info(`Credentials updated for ${this.baseUrl}`);
}
/**
* Define um token de nonce para requisições que necessitam
* @param nonce Token de nonce
*/
setNonce(nonce) {
this.authManager.setNonce(nonce);
this.logger.debug('Nonce token set');
}
/**
* Obtém um cabeçalho da última resposta recebida
* @param header Nome do cabeçalho
* @returns Valor do cabeçalho ou null se não existir
*/
getLastResponseHeader(header) {
// Por simplicidade, vamos retornar null.
// Em uma implementação real, seria necessário armazenar os headers da última resposta
return '0';
}
}
exports.WordPressClient = WordPressClient;
//# sourceMappingURL=WordPressClient.js.map