n8n-nodes-everest-tms
Version:
Everest TMS n8n integration
349 lines • 14.2 kB
JavaScript
;
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