UNPKG

@arathron/n8n-nodes-zoho-books

Version:

n8n community nodes for Zoho Books and Zoho Inventory API integration - Complete CRUD operations for Sales Orders, Invoices, Items, Vendors, Credit Notes, Payments, Purchase Orders, Bills, Composite Items, and Assemblies

316 lines (311 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.checkRateLimit = checkRateLimit; exports.zohoApiRequest = zohoApiRequest; exports.zohoApiRequestWithFormData = zohoApiRequestWithFormData; exports.zohoApiRequestAllItems = zohoApiRequestAllItems; exports.getItemBySku = getItemBySku; const n8n_workflow_1 = require("n8n-workflow"); const DEFAULT_RATE_LIMIT_PER_MINUTE = 100; const DEFAULT_DAILY_LIMIT = 1000; const RETRY_LIMIT = 3; const BACKOFF_BASE = 1000; const rateLimiter = { requests: [], dailyRequests: 0, lastReset: Date.now(), lastRequest: 0, }; async function checkRateLimit(rateLimitPerMinute = DEFAULT_RATE_LIMIT_PER_MINUTE, dailyLimit = DEFAULT_DAILY_LIMIT) { const now = Date.now(); const oneMinuteAgo = now - 60000; if (now - rateLimiter.lastReset > 86400000) { rateLimiter.dailyRequests = 0; rateLimiter.lastReset = now; } rateLimiter.requests = rateLimiter.requests.filter(timestamp => timestamp > oneMinuteAgo); if (rateLimiter.dailyRequests >= dailyLimit) { throw new Error('Daily API limit exceeded'); } if (rateLimiter.requests.length >= rateLimitPerMinute) { const waitTime = 60000 - (now - rateLimiter.requests[0]); if (waitTime > 0) { await new Promise(resolve => setTimeout(resolve, waitTime)); } } rateLimiter.requests.push(now); rateLimiter.dailyRequests++; rateLimiter.lastRequest = now; } function createZohoError(errorResponse) { const code = errorResponse.code || 0; const message = errorResponse.message || 'Unknown error'; const errorMessages = { 1001: { message: 'Resource not found', description: 'The requested resource does not exist or has been deleted.', }, 1002: { message: 'Invalid data provided', description: 'Please check the required fields and data formats.', }, 1003: { message: 'Duplicate entry', description: 'A record with the same information already exists.', }, 4000: { message: 'Rate limit exceeded', description: 'Too many requests. Please wait and try again.', }, 1006: { message: 'Invalid organization', description: 'The organization ID is invalid or inactive.', }, }; const errorInfo = errorMessages[code] || { message: 'Unknown error occurred', description: message || 'Unknown error occurred', }; return { message: errorInfo.message, description: errorInfo.description, httpCode: code >= 4000 ? '429' : '400', }; } function isRateLimitError(error) { return (error?.response?.status === 429 || error?.code === 4000 || (error?.message && error.message.toLowerCase().includes('rate limit'))); } function isAuthError(error) { return error?.response?.status === 401 || error?.httpCode === 401; } function isNetworkError(error) { return (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT' || error?.message?.includes('Network error')); } function isValidationError(error) { return error?.response?.status === 400 || error?.httpCode === 400; } async function zohoApiRequest(method, endpoint, body = {}, qs = {}) { const credentials = await this.getCredentials('zohoInventoryOAuth2'); const dataCenter = credentials.dataCenter; const organizationId = credentials.organizationId; if (!organizationId || organizationId.trim() === '') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Organization ID is missing', description: 'Please configure Organization ID in your Zoho Inventory OAuth2 credentials. You can find it in Zoho Inventory → Settings → Organization Profile.', httpCode: '400', }); } const baseUrlMap = { 'COM': 'https://www.zohoapis.com/inventory/v1', 'IN': 'https://www.zohoapis.in/inventory/v1', 'EU': 'https://www.zohoapis.eu/inventory/v1', 'AU': 'https://www.zohoapis.com.au/inventory/v1', 'CN': 'https://www.zohoapis.com.cn/inventory/v1', }; const baseUrl = baseUrlMap[dataCenter] || baseUrlMap['COM']; const options = { method, uri: `${baseUrl}${endpoint}`, headers: {}, body, qs: { ...qs, organization_id: organizationId, }, json: true, }; let attempt = 0; while (attempt < RETRY_LIMIT) { try { await checkRateLimit(); const response = await this.helpers.requestOAuth2.call(this, 'zohoInventoryOAuth2', options); if (response && typeof response === 'object' && 'code' in response) { if (response.code !== 0) { throw new n8n_workflow_1.NodeApiError(this.getNode(), createZohoError(response)); } } return response; } catch (error) { if (isRateLimitError(error)) { attempt++; if (attempt < RETRY_LIMIT) { const backoffTime = BACKOFF_BASE * Math.pow(2, attempt - 1); await new Promise((resolve) => setTimeout(resolve, backoffTime)); continue; } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Rate limit exceeded', description: 'Too many requests. Please wait and try again.', httpCode: '429', }); } if (isAuthError(error)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Authorization failed - please check your credentials', description: `Authentication failed. This is most commonly due to data center mismatch. 🌍 CURRENT CONFIGURATION: • Data Center: ${dataCenter} (${baseUrl.replace('/inventory/v1', '')}) • Organization ID: ${organizationId} (${organizationId.match(/^\d+$/) ? '✅ Valid format' : '❌ Invalid format'}) ⚠️ COMMON CAUSES: • Data center mismatch (#1 cause of 401 errors) • Invalid Organization ID format • OAuth2 app configured in wrong data center • Insufficient API permissions ✅ IMMEDIATE FIXES: 1. Verify your Zoho Inventory URL matches your data center setting: If your Zoho Inventory URL is https://inventory.zoho.com, keep COM selected 2. Get exact Organization ID from Zoho Inventory Settings > Organization Profile 3. Ensure OAuth2 app exists in https://api-console.zoho.com 4. Restart n8n if you recently updated the node 🔍 DEBUG INFORMATION: • Request Method: ${method} • Full URL: ${options.uri} • Data Center: ${dataCenter} • Organization ID Header: X-com-zoho-inventory-organizationid: ${organizationId} • Expected OAuth2 Bearer Token: Authorization: Bearer [hidden] 🧪 MANUAL TEST: curl -X ${method} "${options.uri}" \\ -H "Authorization: Bearer [YOUR_ACCESS_TOKEN]" \\ -H "X-com-zoho-inventory-organizationid: ${organizationId}"`, httpCode: '401', }); } if (isNetworkError(error)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Network error occurred while connecting to Zoho Inventory API.', description: 'Please check your internet connection and try again.', httpCode: '0', }); } if (isValidationError(error)) { const errorData = error; const zohoErrorMessage = errorData.response?.data?.message || errorData.message || 'Unknown validation error'; throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Bad request - please check your parameters', description: zohoErrorMessage, httpCode: '400', }); } if (error instanceof n8n_workflow_1.NodeApiError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('rate limit') || errorMessage.includes('Rate limit')) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'The service is receiving too many requests from you', description: 'Please wait and try again.', httpCode: '429', }); } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'The service was not able to process your request', description: errorMessage, }); } } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Zoho Inventory API Error: Rate limited', description: 'Failed to complete the request after multiple attempts. Please try again later.', }); } async function zohoApiRequestWithFormData(method, endpoint, formData = {}, qs = {}) { return zohoApiRequest.call(this, method, endpoint, formData, qs); } async function zohoApiRequestAllItems(method, endpoint, body = {}, query = {}) { const returnData = []; let page = 1; const perPage = 200; do { const qs = { ...query, page, per_page: perPage }; const responseData = await zohoApiRequest.call(this, method, endpoint, body, qs); if (!responseData || typeof responseData !== 'object') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Invalid response format from Zoho API', description: 'Expected object response but received invalid data', }); } let items = []; if (responseData.items) { items = responseData.items; } else if (responseData.composite_items) { items = responseData.composite_items; } else if (responseData.assemblies) { items = responseData.assemblies; } else if (responseData.item_groups) { items = responseData.item_groups; } else if (responseData.warehouses) { items = responseData.warehouses; } else if (Array.isArray(responseData)) { items = responseData; } else { break; } returnData.push(...items); if (items.length < perPage) { break; } page++; } while (true); return returnData; } async function getItemBySku(sku) { if (!sku || sku.trim() === '') { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'SKU code is required', description: 'Please provide a valid SKU code to search for items.', httpCode: '400', }); } try { const response = await zohoApiRequest.call(this, 'GET', '/items', {}, { search_text: sku.trim(), per_page: 200 }); if (!response || !response.items || !Array.isArray(response.items)) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Invalid response from Zoho Inventory API', description: 'Expected items array in response but received invalid data.', httpCode: '500', }); } const exactMatches = response.items.filter((item) => item.sku && item.sku.toLowerCase() === sku.toLowerCase()); if (exactMatches.length === 0) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Item with SKU '${sku}' not found`, description: 'No item found with the specified SKU code. Please verify the SKU is correct.', httpCode: '404', }); } if (exactMatches.length > 1) { throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Multiple items found with SKU '${sku}'`, description: `Found ${exactMatches.length} items with the same SKU. Please ensure SKU codes are unique.`, httpCode: '409', }); } return exactMatches[0]; } catch (error) { if (error instanceof n8n_workflow_1.NodeApiError) { throw error; } throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Failed to retrieve item by SKU', description: `Error occurred while searching for item with SKU '${sku}': ${error instanceof Error ? error.message : String(error)}`, httpCode: '500', }); } } //# sourceMappingURL=GenericFunctions.js.map