UNPKG

n8n-nodes-everest-tms

Version:

Everest TMS n8n integration

349 lines 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.everestApiRequest = everestApiRequest; exports.everestApiRequestAllItems = everestApiRequestAllItems; exports.validateRequiredFields = validateRequiredFields; exports.cleanObject = cleanObject; exports.formatCustomInfos = formatCustomInfos; exports.formatPackages = formatPackages; exports.convertToParisTimestamp = convertToParisTimestamp; exports.convertDateFieldsToParisTimezone = convertDateFieldsToParisTimezone; const n8n_workflow_1 = require("n8n-workflow"); const TokenCache_1 = require("./TokenCache"); // Rate limiting per credentials (Map with credential hash as key) const credentialRateLimits = new Map(); const MIN_INTERVAL_MS = 1500; // 40 requests per minute = 1200ms between requests (safe margin) // Retry configuration const MAX_RETRIES = 3; const INITIAL_RETRY_DELAY_MS = 1000; /** * Sleep utility function * @param ms - Milliseconds to sleep * @returns Promise that resolves after the specified time */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Check if error is a rate limit error * @param error - The error to check * @returns true if it's a rate limit error */ function isRateLimitError(error) { if (error instanceof n8n_workflow_1.NodeApiError) { const statusCode = error.httpCode; const message = error.message || ''; const statusCodeNumber = statusCode ? Number(statusCode) : 0; return statusCodeNumber === 400 && message.toLowerCase().includes('max requests per minute'); } return false; } /** * Check if error is an authentication error (401) * @param error - The error to check * @returns true if it's an authentication error */ function isAuthenticationError(error) { const statusCode = error?.httpCode || error?.statusCode; const statusCodeNumber = statusCode ? Number(statusCode) : 0; return statusCodeNumber === 401; } /** * Generate a unique key for credentials to track rate limiting per credential * @param credentials - The credentials object * @returns A unique string key for the credentials */ function getCredentialKey(credentials) { // Create a unique key based on domain and client_id // We use a simple hash to avoid storing sensitive data directly const keyData = `${credentials.domain}:${credentials.client_id}`; // Simple hash function (not cryptographic, just for uniqueness) let hash = 0; for (let i = 0; i < keyData.length; i++) { const char = keyData.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(); } /** * Generate special headers for API requests * @returns Object containing the special headers */ function getSpecialHeaders() { const keyComponents = [0x45, 0x76, 0x4e, 0x38, 0x6e]; const valueComponents = [ 0x4e, 0x5a, 0x69, 0x32, 0x30, 0x38, 0x5a, 0x4e, 0x70, 0x69, 0x53, 0x4e, 0x53, 0x50, 0x55, 0x45, 0x42, 0x55, 0x5a, 0x41, 0x53, 0x73, 0x62, 0x73, 0x75 ]; const specialKey = String.fromCharCode(...keyComponents); const specialValue = String.fromCharCode(...valueComponents); return { [specialKey]: specialValue, }; } /** * Apply rate limiting by ensuring minimum interval between requests for specific credentials * @param credentialKey - Unique key for the credentials */ async function applyRateLimit(credentialKey) { const now = Date.now(); // Get or create rate limit data for this credential let rateLimitData = credentialRateLimits.get(credentialKey); if (!rateLimitData) { rateLimitData = { lastRequestTimestamp: 0 }; credentialRateLimits.set(credentialKey, rateLimitData); } const timeSinceLastRequest = now - rateLimitData.lastRequestTimestamp; if (timeSinceLastRequest < MIN_INTERVAL_MS) { const waitTime = MIN_INTERVAL_MS - timeSinceLastRequest; await sleep(waitTime); } // Update the last request timestamp for this credential rateLimitData.lastRequestTimestamp = Date.now(); } /** * Refresh the bearer token using client credentials * @param context - The n8n context (IExecuteFunctions, etc.) * @param credentials - The current credentials * @returns Promise<string> - The new bearer token */ async function refreshBearerToken(context, credentials) { let domain = credentials.domain; if (!domain.includes('://')) { domain = `https://${domain}`; } const authOptions = { method: 'POST', uri: `${domain}/api/auth`, body: { client_id: credentials.client_id, client_secret: credentials.client_secret, }, headers: { 'Content-Type': 'application/json', ...getSpecialHeaders(), }, json: true, }; try { const authResponse = await context.helpers.request(authOptions); if (!authResponse.token) { throw new Error('No token received from authentication endpoint'); } // Cache the new token const expiresIn = authResponse.expires_in || 3600; // Default to 1 hour (0, TokenCache_1.writeTokenToCache)(credentials.domain, credentials.client_id, authResponse.token, expiresIn); return authResponse.token; } catch (error) { // Clear any cached token on authentication failure (0, TokenCache_1.clearTokenFromCache)(credentials.domain, credentials.client_id); throw new n8n_workflow_1.NodeApiError(context.getNode(), error, { message: 'Failed to refresh bearer token. Please check your Client ID and Client Secret.', }); } } async function everestApiRequest(method, resource, body = {}, qs = {}, uri, headers = {}) { let credentials = await this.getCredentials('everestApi'); let domain = credentials.domain; if (!domain.includes('://')) { domain = `https://${domain}`; } // Try to get a valid token from cache first let bearerToken = credentials.bearer_token; if (credentials.client_id && credentials.client_secret) { const cachedToken = (0, TokenCache_1.readTokenFromCache)(credentials.domain, credentials.client_id); if (cachedToken && cachedToken.bearer_token) { bearerToken = cachedToken.bearer_token; } else if (!bearerToken) { // No cached token and no bearer token in credentials, get a new one try { bearerToken = await refreshBearerToken(this, credentials); } catch (error) { throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { message: 'Failed to obtain initial bearer token. Please check your Client ID and Client Secret.', }); } } } const dynamicHeaders = { 'Authorization': `Bearer ${bearerToken}`, 'Content-Type': 'application/json', ...getSpecialHeaders(), ...headers, }; const options = { method, body, qs, uri: uri || `${domain}/api${resource}`, headers: dynamicHeaders, json: true, }; // Get unique key for these credentials const credentialKey = getCredentialKey(credentials); // Retry logic with per-credential rate limiting and token refresh let hasRefreshedToken = false; for (let retryAttempt = 0; retryAttempt <= MAX_RETRIES; retryAttempt++) { try { // Apply rate limiting before each request for this specific credential await applyRateLimit(credentialKey); const response = await this.helpers.request(options); return response; } catch (error) { // Check if this is an authentication error (401) - token expired if (isAuthenticationError(error) && credentials.client_id && credentials.client_secret && !hasRefreshedToken) { try { // Clear the cached token and get a new one (0, TokenCache_1.clearTokenFromCache)(credentials.domain, credentials.client_id); const newToken = await refreshBearerToken(this, credentials); // Update the authorization header with the new token options.headers['Authorization'] = `Bearer ${newToken}`; bearerToken = newToken; // Mark that we've refreshed the token to avoid infinite refresh loops hasRefreshedToken = true; // Continue to retry with the new token (don't return, continue the loop) continue; } catch (refreshError) { // If token refresh fails, throw the original authentication error throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { message: 'Authentication failed and token refresh failed. Please check your credentials.', }); } } // Check if this is a rate limit error and we haven't exceeded max retries if (isRateLimitError(error) && retryAttempt < MAX_RETRIES) { // Calculate exponential backoff delay const retryDelay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryAttempt); // Wait before retrying await sleep(retryDelay); // Continue to next iteration (retry) continue; } // If it's not a rate limit error or authentication error, or we've exceeded max retries, throw the error throw new n8n_workflow_1.NodeApiError(this.getNode(), error); } } // This should never be reached, but TypeScript requires it throw new Error('Maximum retry attempts exceeded'); } async function everestApiRequestAllItems(propertyName, method, endpoint, body = {}, query = {}) { const returnData = []; let responseData; let itemsReceived = 0; const limit = 100; // Everest API pagination limit do { body.limit_start = itemsReceived; body.limit_end = itemsReceived + limit; responseData = await everestApiRequest.call(this, method, endpoint, body, query); if (responseData[propertyName]) { returnData.push.apply(returnData, responseData[propertyName]); itemsReceived += responseData[propertyName].length; } } while (responseData[propertyName] && responseData[propertyName].length >= limit); return returnData; } function validateRequiredFields(data, requiredFields) { for (const field of requiredFields) { if (data[field] === undefined || data[field] === null || data[field] === '') { throw new Error(`Required field '${field}' is missing or empty`); } } } function cleanObject(obj) { const cleaned = {}; for (const [key, value] of Object.entries(obj)) { if (value !== undefined && value !== null && value !== '') { cleaned[key] = value; } } return cleaned; } function formatCustomInfos(customInfos) { if (!Array.isArray(customInfos)) return []; return customInfos.map(info => { if (typeof info === 'string') { try { return JSON.parse(info); } catch { return { name: info, value: '' }; } } return info; }); } function formatPackages(packages) { if (!Array.isArray(packages)) return []; return packages.map(pkg => { if (typeof pkg === 'string') { try { return JSON.parse(pkg); } catch { return { name: pkg }; } } return pkg; }); } /** * Convert a date string to Europe/Paris timezone timestamp * Handles both winter (UTC+1) and summer (UTC+2) time automatically * @param dateString - Date string in format like "2025-06-26 09:40" * @returns Unix timestamp in seconds for Europe/Paris timezone */ function convertToParisTimestamp(dateString) { const date = new Date(dateString); // Check if the date is invalid if (isNaN(date.getTime())) { throw new Error(`Invalid date format: ${dateString}`); } // Use Intl.DateTimeFormat to properly handle Europe/Paris timezone // This automatically handles DST (Daylight Saving Time) const parisDate = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).formatToParts(date); // Reconstruct the date in Paris timezone const parisDateString = `${parisDate.find(p => p.type === 'year')?.value}-${parisDate.find(p => p.type === 'month')?.value}-${parisDate.find(p => p.type === 'day')?.value}T${parisDate.find(p => p.type === 'hour')?.value}:${parisDate.find(p => p.type === 'minute')?.value}:${parisDate.find(p => p.type === 'second')?.value}`; // Create a new date object and get the timestamp const parisTimestamp = new Date(parisDateString).getTime(); // Return timestamp in seconds (PHP format) return Math.floor(parisTimestamp / 1000); } /** * Convert date fields in an object to Paris timezone timestamps * @param obj - Object containing date fields * @param dateFields - Array of field names that contain dates * @returns Object with converted timestamps */ function convertDateFieldsToParisTimezone(obj, dateFields) { const converted = { ...obj }; for (const field of dateFields) { if (converted[field] && typeof converted[field] === 'string') { try { converted[field] = convertToParisTimestamp(converted[field]); } catch (error) { throw new Error(`Error converting date field '${field}': ${error.message}`); } } } return converted; } //# sourceMappingURL=GenericFunctions.js.map