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
JavaScript
;
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