UNPKG

n8n-nodes-tiny-erp-api-v2

Version:

Custom nodes for Tiny ERP integration with n8n, including AI tools for AI Agent workflows

281 lines 12.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TinyErpApi = void 0; const bottleneck_1 = __importDefault(require("bottleneck")); const PLAN_LIMITS = { COMECAR: { callsPerMinute: 0, batchCallsPerMinute: 0 }, CRESCER: { callsPerMinute: 30, batchCallsPerMinute: 5 }, EVOLUIR: { callsPerMinute: 60, batchCallsPerMinute: 5 }, POTENCIALIZAR: { callsPerMinute: 120, batchCallsPerMinute: 5 }, DEFAULT: { callsPerMinute: 30, batchCallsPerMinute: 5 }, }; class TinyErpApi { constructor(executeFunctions, credentials) { var _a; this.lastRateLimitInfo = null; this.executeFunctions = executeFunctions; this.credentials = credentials; const planName = ((_a = process.env.TINY_PLAN) !== null && _a !== void 0 ? _a : 'CRESCER').toUpperCase(); const plan = PLAN_LIMITS[planName] || PLAN_LIMITS.DEFAULT; if (!TinyErpApi.limiterMap.has(credentials.token)) { TinyErpApi.limiterMap.set(credentials.token, new bottleneck_1.default({ minTime: plan.callsPerMinute > 0 ? Math.ceil(60000 / plan.callsPerMinute) : 60000, maxConcurrent: 1, reservoir: plan.callsPerMinute, reservoirRefreshInterval: 60000, reservoirRefreshAmount: plan.callsPerMinute, })); } if (!TinyErpApi.batchLimiterMap.has(credentials.token)) { TinyErpApi.batchLimiterMap.set(credentials.token, new bottleneck_1.default({ minTime: plan.batchCallsPerMinute > 0 ? Math.ceil(60000 / plan.batchCallsPerMinute) : 60000, maxConcurrent: 1, reservoir: plan.batchCallsPerMinute, reservoirRefreshInterval: 60000, reservoirRefreshAmount: plan.batchCallsPerMinute, })); } this.limiter = TinyErpApi.limiterMap.get(credentials.token); this.batchLimiter = TinyErpApi.batchLimiterMap.get(credentials.token); } async retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; const isRateLimitError = error.statusCode === 429 || error.message.includes('429') || error.message.toLowerCase().includes('rate limit') || error.message.toLowerCase().includes('too many requests'); if (!isRateLimitError || attempt === maxRetries) { throw error; } const delay = baseDelay * Math.pow(2, attempt); console.log(`🔍 [Tiny ERP API] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } getCacheKey(endpoint, data) { const dataStr = data ? JSON.stringify(data) : ''; return `${this.credentials.token}:${endpoint}:${dataStr}`; } getFromCache(cacheKey) { const entry = TinyErpApi.cacheMap.get(cacheKey); if (!entry) return null; const now = Date.now(); if (now > entry.timestamp + entry.ttl) { TinyErpApi.cacheMap.delete(cacheKey); return null; } console.log('🔍 [Tiny ERP API] Cache hit for:', cacheKey); return entry.data; } setCache(cacheKey, data, ttlMs = 30000) { TinyErpApi.cacheMap.set(cacheKey, { data, timestamp: Date.now(), ttl: ttlMs, }); } updateRateLimiterFromHeaders(headers) { const limit = Number(headers['x-limit-api']); const remaining = Number(headers['x-remaining-api']) || limit; if (limit && limit > 0) { console.log(`🔍 [Tiny ERP API] API reports rate limit: ${limit} calls/minute, remaining: ${remaining}`); this.limiter.updateSettings({ minTime: Math.ceil(60000 / limit), reservoir: limit, reservoirRefreshAmount: limit, }); this.lastRateLimitInfo = { limit, remaining, resetTime: Date.now() + 60000, }; } } isBatchOperation(endpoint) { const batchEndpoints = [ 'contato.incluir.php', 'contato.alterar.php', 'grupo.tag.incluir.php', 'grupo.tag.alterar.php', 'tag.incluir.php', 'tag.alterar.php', 'produto.incluir.php', 'produto.alterar.php', ]; return batchEndpoints.includes(endpoint); } isCacheableEndpoint(endpoint) { const cacheableEndpoints = [ 'produtos.pesquisa.php', 'produto.obter.php', 'produto.obter.estoque.php', 'pedido.obter.php', 'contatos.pesquisar.php', ]; return cacheableEndpoints.includes(endpoint); } async rawRequest(endpoint, method = 'POST', data) { const baseUrl = 'https://api.tiny.com.br/api2'; const url = `${baseUrl}/${endpoint}`; const formData = new URLSearchParams(); formData.append('token', this.credentials.token); formData.append('formato', 'json'); if (data) { Object.entries(data).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { if (typeof value === 'number' && (key === 'idTag' || key === 'idListaPreco') && value <= 0) { return; } formData.append(key, String(value)); } }); } console.log('🔍 [Tiny ERP API] Making request to:', url); console.log('🔍 [Tiny ERP API] Method:', method); const options = { method, url, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData.toString(), json: false, resolveWithFullResponse: true, }; try { const response = await this.executeFunctions.helpers.request(options); this.updateRateLimiterFromHeaders(response.headers); let parsedBody; if (typeof response.body === 'string') { try { parsedBody = JSON.parse(response.body); } catch (parseError) { throw new Error(`Failed to parse JSON response: ${parseError.message}. Response: ${response.body}`); } } else { parsedBody = response.body; } if (this.lastRateLimitInfo) { parsedBody._rateLimit = this.lastRateLimitInfo; } return parsedBody; } catch (error) { if (error.statusCode === 429) { console.log('🔍 [Tiny ERP API] Rate limit exceeded, will retry...'); throw error; } throw new Error(`Tiny ERP API request failed: ${error.message}`); } } async makeRequest(endpoint, method = 'POST', data) { if (method === 'POST' && this.isCacheableEndpoint(endpoint)) { const cacheKey = this.getCacheKey(endpoint, data); const cachedData = this.getFromCache(cacheKey); if (cachedData) { return cachedData; } } const limiter = this.isBatchOperation(endpoint) ? this.batchLimiter : this.limiter; const result = await limiter.schedule(() => this.retryWithBackoff(() => this.rawRequest(endpoint, method, data))); if (method === 'POST' && this.isCacheableEndpoint(endpoint) && result) { const cacheKey = this.getCacheKey(endpoint, data); const ttl = endpoint.includes('produto') ? 300000 : 30000; this.setCache(cacheKey, result, ttl); } return result; } async getProducts(params = {}) { console.log('🔍 [Tiny ERP API] getProducts called with params:', JSON.stringify(params, null, 2)); return this.makeRequest('produtos.pesquisa.php', 'POST', params); } async getOrderData(orderNumber) { console.log('🔍 [Tiny ERP API] getOrderData called with orderNumber:', orderNumber); return this.makeRequest('pedido.obter.php', 'POST', { numero: orderNumber }); } async getUserData(params = {}) { console.log('🔍 [Tiny ERP API] getUserData called with params:', JSON.stringify(params, null, 2)); return this.makeRequest('contatos.pesquisar.php', 'POST', params); } async getProductStock(productId) { console.log('🔍 [Tiny ERP API] getProductStock called with productId:', productId); return this.makeRequest('produto.obter.estoque.php', 'POST', { id: productId }); } async createProduct(productData) { console.log('🔍 [Tiny ERP API] createProduct called with productData:', JSON.stringify(productData, null, 2)); const produtoJson = JSON.stringify(productData); console.log('🔍 [Tiny ERP API] produto JSON:', produtoJson); return this.makeRequest('produto.incluir.php', 'POST', { produto: produtoJson }); } async createProductsBatch(productsData) { console.log('🔍 [Tiny ERP API] createProductsBatch called with', productsData.length, 'products'); const batchSize = 20; const results = []; for (let i = 0; i < productsData.length; i += batchSize) { const batch = productsData.slice(i, i + batchSize); const batchData = { produtos: batch.map(product => ({ produto: product })) }; const produtoJson = JSON.stringify(batchData); console.log(`🔍 [Tiny ERP API] Creating batch ${Math.floor(i / batchSize) + 1} with ${batch.length} products`); const result = await this.makeRequest('produto.incluir.php', 'POST', { produto: produtoJson }); results.push(result); } return results; } async updateStock(stockData) { console.log('🔍 [Tiny ERP API] updateStock called with stockData:', JSON.stringify(stockData, null, 2)); const estoqueWrapper = { estoque: stockData }; const estoqueJson = JSON.stringify(estoqueWrapper); console.log('🔍 [Tiny ERP API] estoque JSON (wrapped):', estoqueJson); return this.makeRequest('produto.atualizar.estoque.php', 'POST', { estoque: estoqueJson }); } async updateStockBatch(stockDataArray) { console.log('🔍 [Tiny ERP API] updateStockBatch called with', stockDataArray.length, 'stock updates'); const batchSize = 20; const results = []; for (let i = 0; i < stockDataArray.length; i += batchSize) { const batch = stockDataArray.slice(i, i + batchSize); const batchData = { estoques: batch.map(stock => ({ estoque: stock })) }; const estoqueJson = JSON.stringify(batchData); console.log(`🔍 [Tiny ERP API] Updating stock batch ${Math.floor(i / batchSize) + 1} with ${batch.length} items`); const result = await this.makeRequest('produto.atualizar.estoque.php', 'POST', { estoque: estoqueJson }); results.push(result); } return results; } getRateLimitInfo() { return this.lastRateLimitInfo; } static clearCache() { TinyErpApi.cacheMap.clear(); console.log('🔍 [Tiny ERP API] Cache cleared'); } static getCacheStats() { return { size: TinyErpApi.cacheMap.size, entries: Array.from(TinyErpApi.cacheMap.keys()), }; } } exports.TinyErpApi = TinyErpApi; TinyErpApi.limiterMap = new Map(); TinyErpApi.batchLimiterMap = new Map(); TinyErpApi.cacheMap = new Map(); //# sourceMappingURL=api.js.map