n8n-nodes-heyreach
Version:
n8n community node for HeyReach API - LinkedIn automation platform
336 lines • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.heyReachApiRequest = heyReachApiRequest;
exports.heyReachApiRequestAllItems = heyReachApiRequestAllItems;
exports.validateCampaignStatus = validateCampaignStatus;
exports.validateLinkedInProfileUrl = validateLinkedInProfileUrl;
exports.getLinkedInUrlValidationError = getLinkedInUrlValidationError;
exports.validateCustomFieldName = validateCustomFieldName;
exports.formatDateForApi = formatDateForApi;
exports.validateAndParseId = validateAndParseId;
exports.validateDateRange = validateDateRange;
exports.parseAndValidateIds = parseAndValidateIds;
exports.sanitizeSearchKeyword = sanitizeSearchKeyword;
exports.validatePaginationParams = validatePaginationParams;
const n8n_workflow_1 = require("n8n-workflow");
/**
* Make an API request to HeyReach
*/
async function heyReachApiRequest(method, endpoint, body = {}, qs = {}) {
const requestStartTime = Date.now();
console.log('🌐 heyReachApiRequest() - Starting API request at:', new Date().toISOString());
console.log('🌐 heyReachApiRequest() - Method:', method);
console.log('🌐 heyReachApiRequest() - Endpoint:', endpoint);
console.log('🌐 heyReachApiRequest() - Body:', JSON.stringify(body, null, 2));
const credentials = await this.getCredentials('heyReachApi');
if (credentials === undefined) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No credentials got returned!');
}
const options = {
headers: {
'X-API-KEY': credentials.apiKey,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
method,
body,
qs,
url: `https://api.heyreach.io/api/public${endpoint}`,
json: true,
timeout: 60000, // 60 second timeout for HeyReach API (as per documentation)
};
// Remove empty body for GET requests
if (method === 'GET') {
delete options.body;
}
console.log('🌐 heyReachApiRequest() - Full URL:', options.url);
console.log('🌐 heyReachApiRequest() - Headers:', JSON.stringify(options.headers, null, 2));
console.log('🌐 heyReachApiRequest() - Timeout:', options.timeout, 'ms');
try {
console.log('🚀 heyReachApiRequest() - Making HTTP request...');
const httpStartTime = Date.now();
const response = await this.helpers.request(options);
const httpEndTime = Date.now();
const totalTime = httpEndTime - requestStartTime;
const httpTime = httpEndTime - httpStartTime;
console.log('✅ heyReachApiRequest() - HTTP request completed successfully');
console.log('⏱️ heyReachApiRequest() - HTTP request took:', httpTime, 'ms');
console.log('⏱️ heyReachApiRequest() - Total API request took:', totalTime, 'ms');
console.log('📦 heyReachApiRequest() - Response type:', typeof response);
console.log('📦 heyReachApiRequest() - Response keys:', Object.keys(response || {}));
return response;
}
catch (error) {
const errorTime = Date.now();
const totalTime = errorTime - requestStartTime;
console.error('❌ heyReachApiRequest() - HTTP request failed');
console.error('⏱️ heyReachApiRequest() - Failed request took:', totalTime, 'ms');
console.error('💥 heyReachApiRequest() - Error type:', error.constructor.name);
console.error('💥 heyReachApiRequest() - Error message:', error.message);
console.error('💥 heyReachApiRequest() - Error status:', error.response?.status);
console.error('💥 heyReachApiRequest() - Error body:', error.response?.body);
// Handle HeyReach-specific errors
if (error.response?.body) {
const errorBody = error.response.body;
// Handle different error formats
if (typeof errorBody === 'string') {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
message: errorBody,
description: `HeyReach API Error: ${errorBody}`,
});
}
if (errorBody.error || errorBody.message) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
message: errorBody.error || errorBody.message,
description: `HeyReach API Error: ${errorBody.error || errorBody.message}`,
});
}
}
// Handle HTTP status codes
if (error.response?.status) {
const status = error.response.status;
let message = `HTTP ${status} Error`;
let description = '';
switch (status) {
case 401:
message = 'Authentication failed';
description = 'Please check your HeyReach API key in the credentials.';
break;
case 403:
message = 'Access forbidden';
description = 'Your API key does not have permission to access this resource.';
break;
case 404:
message = 'Resource not found';
description = 'The requested endpoint or resource does not exist. This may indicate an API documentation discrepancy.';
break;
case 429:
message = 'Rate limit exceeded';
description = 'HeyReach allows a maximum of 300 requests per minute. Please wait before making more requests.';
break;
case 400:
// Special handling for campaign pause/resume operations
// Allow these specific errors to bubble up to CampaignOperations for custom handling
if (error.message && (error.message.includes('You cannot pause an inactive campaign') ||
error.message.includes('cannot resume') ||
error.message.includes('not paused, finished or failed'))) {
console.log('🔄 heyReachApiRequest() - Allowing campaign operation error to bubble up for custom handling');
throw error; // Re-throw the original error, not a NodeApiError
}
message = 'Bad request';
description = 'The request was invalid. Please check your parameters and try again.';
break;
case 500:
message = 'Internal server error';
description = 'HeyReach API is experiencing issues. Please try again later.';
break;
}
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
message,
description,
});
}
// Generic error handling
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
}
}
/**
* Make an API request to HeyReach with automatic pagination
* This function fetches ALL available items (ignores any limit in body)
*/
async function heyReachApiRequestAllItems(method, endpoint, body = {}, qs = {}) {
const returnData = [];
let responseData;
// Set initial pagination parameters
body.offset = body.offset || 0;
body.limit = 100; // Always use API maximum for "get all" operations
do {
responseData = await heyReachApiRequest.call(this, method, endpoint, body, qs);
if (responseData.items && Array.isArray(responseData.items)) {
returnData.push(...responseData.items);
// Update offset for next request
body.offset = body.offset + body.limit;
// Check if we have more items to fetch
if (responseData.items.length < body.limit ||
returnData.length >= (responseData.totalCount || 0)) {
break;
}
}
else {
// If response doesn't have items array, return the response as is
returnData.push(responseData);
break;
}
} while (true);
return returnData;
}
/**
* Validate campaign status for operations that require ACTIVE campaigns
*/
function validateCampaignStatus(campaign, operation) {
if (!campaign) {
throw new n8n_workflow_1.NodeOperationError({}, 'Campaign not found. Please check the campaign ID.');
}
const validStatuses = ['IN_PROGRESS', 'PAUSED'];
if (!validStatuses.includes(campaign.status)) {
throw new n8n_workflow_1.NodeOperationError({}, `Cannot ${operation} for campaign with status "${campaign.status}". Campaign must be IN_PROGRESS or PAUSED. Current status: ${campaign.status}`);
}
if (!campaign.campaignAccountIds || campaign.campaignAccountIds.length === 0) {
throw new n8n_workflow_1.NodeOperationError({}, `Cannot ${operation} for campaign "${campaign.name}". Campaign must have LinkedIn accounts assigned.`);
}
}
/**
* Validate LinkedIn profile URL format
* Supports international characters, numbers, hyphens, and common LinkedIn profile formats
*/
function validateLinkedInProfileUrl(url) {
// Handle null/undefined/empty strings
if (!url || typeof url !== 'string' || url.trim() === '') {
console.log('validateLinkedInProfileUrl() - Invalid input: URL is null, undefined, or empty');
return false;
}
// Trim whitespace
const trimmedUrl = url.trim();
// Updated pattern to support:
// - International/Unicode characters (\p{L} for letters)
// - Numbers (\p{N})
// - Hyphens and common punctuation
// - LinkedIn's various profile URL formats
const linkedInUrlPattern = /^https:\/\/(www\.)?linkedin\.com\/in\/[\p{L}\p{N}\-_.%]+\/?$/u;
let isValid = false;
try {
isValid = linkedInUrlPattern.test(trimmedUrl);
}
catch (error) {
console.log('validateLinkedInProfileUrl() - Regex test failed:', error);
// Fallback to simpler pattern if Unicode regex fails
const fallbackPattern = /^https:\/\/(www\.)?linkedin\.com\/in\/[a-zA-Z0-9\-_.%]+\/?$/;
isValid = fallbackPattern.test(trimmedUrl);
console.log('validateLinkedInProfileUrl() - Using fallback pattern, result:', isValid);
}
// Debug: Log validation details
console.log('validateLinkedInProfileUrl() - URL:', JSON.stringify(trimmedUrl));
console.log('validateLinkedInProfileUrl() - Pattern:', linkedInUrlPattern.toString());
console.log('validateLinkedInProfileUrl() - Is valid:', isValid);
return isValid;
}
/**
* Get detailed LinkedIn profile URL validation error message
*/
function getLinkedInUrlValidationError(url) {
if (!url || typeof url !== 'string' || url.trim() === '') {
return 'LinkedIn Profile URL is required and cannot be empty';
}
const trimmedUrl = url.trim();
if (!trimmedUrl.startsWith('https://')) {
return `LinkedIn Profile URL must start with "https://". Current URL: "${trimmedUrl}"`;
}
if (!trimmedUrl.includes('linkedin.com/in/')) {
return `LinkedIn Profile URL must contain "linkedin.com/in/". Current URL: "${trimmedUrl}"`;
}
// Check for common issues
if (trimmedUrl.includes('?')) {
return `LinkedIn Profile URL should not contain query parameters. Remove everything after "?" from: "${trimmedUrl}"`;
}
if (trimmedUrl.includes('#')) {
return `LinkedIn Profile URL should not contain hash fragments. Remove everything after "#" from: "${trimmedUrl}"`;
}
// If we get here, it's a more complex validation issue
return `LinkedIn Profile URL format is invalid. Expected format: "https://www.linkedin.com/in/username". Current URL: "${trimmedUrl}"`;
}
/**
* Validate custom user field names (alphanumeric + underscore only)
*/
function validateCustomFieldName(name) {
const validNamePattern = /^[a-zA-Z0-9_]+$/;
return validNamePattern.test(name);
}
/**
* Format date for HeyReach API (ISO 8601 format)
*/
function formatDateForApi(date) {
if (typeof date === 'string') {
return new Date(date).toISOString();
}
return date.toISOString();
}
/**
* Validate and parse integer ID from string or number
*/
function validateAndParseId(id, fieldName) {
if (typeof id === 'number') {
return id;
}
if (typeof id === 'string') {
const parsed = parseInt(id, 10);
if (isNaN(parsed) || parsed <= 0) {
throw new Error(`Invalid ${fieldName}: ${id}. Must be a positive integer.`);
}
return parsed;
}
throw new Error(`${fieldName} is required and must be a number.`);
}
/**
* Validate date range
*/
function validateDateRange(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (isNaN(start.getTime())) {
throw new Error(`Invalid start date format: ${startDate}`);
}
if (isNaN(end.getTime())) {
throw new Error(`Invalid end date format: ${endDate}`);
}
if (start >= end) {
throw new Error('Start date must be before end date');
}
// Check if date range is not too far in the future
const now = new Date();
if (start > now) {
throw new Error('Start date cannot be in the future');
}
}
/**
* Parse comma-separated IDs and validate them
*/
function parseAndValidateIds(idsString, fieldName) {
if (!idsString || idsString.trim().length === 0) {
return [];
}
const ids = idsString
.split(',')
.map(id => id.trim())
.filter(id => id.length > 0)
.map(id => {
const parsed = parseInt(id, 10);
if (isNaN(parsed) || parsed <= 0) {
throw new Error(`Invalid ${fieldName}: ${id}. All IDs must be positive integers.`);
}
return parsed;
});
// Remove duplicates
return [...new Set(ids)];
}
/**
* Sanitize search keyword for API requests
*/
function sanitizeSearchKeyword(keyword) {
if (!keyword || typeof keyword !== 'string') {
return '';
}
// Remove special characters that might cause API issues
return keyword
.trim()
.replace(/[<>\"'&]/g, '') // Remove potentially dangerous characters
.substring(0, 100); // Limit length
}
/**
* Validate pagination parameters
*/
function validatePaginationParams(limit, returnAll) {
if (!returnAll && (limit <= 0 || limit > 100)) {
throw new Error('Limit must be between 1 and 100 when not returning all results');
}
}
//# sourceMappingURL=generic.functions.js.map