UNPKG

@ai-growth/n8n-nodes-wordpress

Version:

n8n node for WordPress integration with AI GROWTH - SEO WP plugin

652 lines 29.1 kB
"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