n8n-nodes-instantly
Version:
n8n community node for Instantly API v2
616 lines • 26.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstantlyApi = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const generic_functions_1 = require("../generic.functions");
const OperationRouter_1 = require("./operations/OperationRouter");
const LeadParameters_1 = require("./parameters/LeadParameters");
const CampaignParameters_1 = require("./parameters/CampaignParameters");
// Helper function to format dates for Instantly API (YYYY-MM-DD format)
function formatDateForApi(dateInput) {
if (!dateInput || dateInput === '') {
return '';
}
let dateString = String(dateInput);
// Handle n8n DateTime objects that come as "[DateTime: 2025-06-26T13:52:08.271Z]"
if (dateString.startsWith('[DateTime: ') && dateString.endsWith(']')) {
dateString = dateString.slice(11, -1); // Remove "[DateTime: " and "]"
}
// Handle ISO datetime strings (e.g., "2025-06-19T13:52:45.316Z")
if (dateString.includes('T')) {
dateString = dateString.split('T')[0];
}
// Handle date strings that might have time components separated by space
if (dateString.includes(' ')) {
dateString = dateString.split(' ')[0];
}
// Validate the resulting date format (should be YYYY-MM-DD)
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(dateString)) {
// Try to parse as Date and format
try {
const parsedDate = new Date(dateInput);
if (!isNaN(parsedDate.getTime())) {
// Format as YYYY-MM-DD
const year = parsedDate.getFullYear();
const month = String(parsedDate.getMonth() + 1).padStart(2, '0');
const day = String(parsedDate.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
}
catch (error) {
console.warn('Failed to parse date:', dateInput, error);
}
// If all parsing fails, return empty string to avoid API errors
console.warn('Invalid date format for Instantly API:', dateInput);
return '';
}
return dateString;
}
// Helper function for pagination
async function paginateInstantlyApi(context, endpoint, resourceName) {
let allItems = [];
let startingAfter;
let hasMore = true;
let pageCount = 0;
console.log(`Starting pagination for ${resourceName}...`);
while (hasMore) {
pageCount++;
const queryParams = { limit: 100 }; // Use max limit for efficiency
if (startingAfter) {
queryParams.starting_after = startingAfter;
}
console.log(`Fetching page ${pageCount} with params:`, queryParams);
const response = await generic_functions_1.instantlyApiRequest.call(context, 'GET', endpoint, {}, queryParams);
console.log(`Page ${pageCount} response:`, {
hasItems: !!response.items,
itemsLength: response.items?.length || 0,
hasNextStartingAfter: !!response.next_starting_after,
nextStartingAfter: response.next_starting_after
});
// Handle response structure - items are in 'items' array for paginated responses
let itemsData = [];
if (response.items && Array.isArray(response.items)) {
itemsData = response.items;
}
else if (response.data && Array.isArray(response.data)) {
itemsData = response.data;
}
else if (Array.isArray(response)) {
itemsData = response;
}
else {
console.warn('Unexpected response structure:', response);
}
if (itemsData.length > 0) {
allItems = allItems.concat(itemsData);
console.log(`Added ${itemsData.length} ${resourceName}. Total so far: ${allItems.length}`);
}
else {
console.log(`No ${resourceName} found in this page, stopping pagination`);
hasMore = false;
}
// Check if there are more pages using next_starting_after
if (response.next_starting_after && itemsData.length > 0) {
startingAfter = response.next_starting_after;
console.log(`Next page cursor: ${startingAfter}`);
}
else {
console.log('No more pages available');
hasMore = false;
}
// Safety check to prevent infinite loops
if (pageCount > 1000) {
console.warn('Pagination stopped after 1000 pages to prevent infinite loop');
break;
}
}
console.log(`Pagination completed. Total ${resourceName} retrieved: ${allItems.length}`);
return allItems;
}
// Helper function to get campaigns for dropdown
async function getCampaigns(filter) {
try {
// Get campaigns using our existing pagination function
const responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', '/api/v2/campaigns', {}, { limit: 100 });
// Handle response structure - campaigns are in 'items' array for paginated responses
let campaigns = [];
if (responseData.items && Array.isArray(responseData.items)) {
campaigns = responseData.items;
}
else if (Array.isArray(responseData)) {
campaigns = responseData;
}
// Filter campaigns if filter is provided
const filteredCampaigns = campaigns.filter((campaign) => {
if (!filter)
return true;
const campaignName = `${campaign.name || ''}`.toLowerCase();
return campaignName.includes(filter.toLowerCase());
});
// Map campaigns to dropdown options
const campaignOptions = filteredCampaigns.map((campaign) => ({
name: campaign.name || `Campaign ${campaign.id}`,
value: campaign.id,
}));
return {
results: campaignOptions,
};
}
catch (error) {
console.error('Error fetching campaigns for dropdown:', error);
return {
results: [],
};
}
}
// Helper function to get email accounts for dropdown
async function getEmailAccounts(filter) {
try {
// Get email accounts using the accounts API endpoint
const responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', '/api/v2/accounts', {}, { limit: 100 });
// Handle response structure - accounts are in 'items' array for paginated responses
let accounts = [];
if (responseData.items && Array.isArray(responseData.items)) {
accounts = responseData.items;
}
else if (Array.isArray(responseData)) {
accounts = responseData;
}
// Filter accounts if filter is provided
const filteredAccounts = accounts.filter((account) => {
if (!filter)
return true;
const accountEmail = `${account.email || ''}`.toLowerCase();
const accountName = `${account.first_name || ''} ${account.last_name || ''}`.toLowerCase().trim();
return accountEmail.includes(filter.toLowerCase()) || accountName.includes(filter.toLowerCase());
});
// Map accounts to dropdown options
const accountOptions = filteredAccounts.map((account) => {
const displayName = account.first_name && account.last_name
? `${account.email} (${account.first_name} ${account.last_name})`
: account.email;
return {
name: displayName || `Account ${account.id}`,
value: account.email,
};
});
return {
results: accountOptions,
};
}
catch (error) {
console.error('Error fetching email accounts for dropdown:', error);
return {
results: [],
};
}
}
// Helper function to get email accounts for multiOptions selection
async function getEmailAccountsForSelection() {
console.log('🔍 getEmailAccountsForSelection called!');
try {
// Get email accounts using the accounts API endpoint
const responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', '/api/v2/accounts', {}, { limit: 100 });
console.log('📧 Email accounts response:', responseData);
// Handle response structure - accounts are in 'items' array for paginated responses
let accounts = [];
if (responseData.items && Array.isArray(responseData.items)) {
accounts = responseData.items;
}
else if (Array.isArray(responseData)) {
accounts = responseData;
}
console.log(`📋 Found ${accounts.length} email accounts`);
// Map accounts to multiOptions format
const accountOptions = accounts.map((account) => {
const displayName = account.first_name && account.last_name
? `${account.email} (${account.first_name} ${account.last_name})`
: account.email;
return {
name: displayName || `Account ${account.id}`,
value: account.email,
};
});
console.log('✅ Returning account options:', accountOptions);
return accountOptions;
}
catch (error) {
console.error('❌ Error fetching email accounts for selection:', error);
return [];
}
}
// Helper function to get leads for dropdown
async function getLeads(filter) {
try {
// Get leads using the leads list API endpoint
const body = {
limit: 100,
};
// Add search filter if provided
if (filter) {
body.search = filter;
}
const response = await generic_functions_1.instantlyApiRequest.call(this, 'POST', '/api/v2/leads/list', body);
const leadOptions = [];
if (response.items && Array.isArray(response.items)) {
for (const lead of response.items) {
const name = `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || lead.email || 'Unknown Lead';
const value = lead.id;
leadOptions.push({
name: `${name} (${lead.email || 'No email'})`,
value,
});
}
}
return {
results: leadOptions,
};
}
catch (error) {
console.error('Error fetching leads for dropdown:', error);
return {
results: [],
};
}
}
class InstantlyApi {
constructor() {
this.description = {
displayName: 'Instantly',
name: 'instantly',
icon: 'file:instantly.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Interact with Instantly API',
defaults: {
name: 'Instantly',
},
inputs: ["main" /* NodeConnectionType.Main */],
outputs: ["main" /* NodeConnectionType.Main */],
credentials: [
{
name: 'instantlyApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Account',
value: 'account',
},
{
name: 'Analytics',
value: 'analytics',
},
{
name: 'Campaign',
value: 'campaign',
},
{
name: 'Lead',
value: 'lead',
},
],
default: 'campaign',
},
// CAMPAIGN OPERATIONS - Using modular parameters
...CampaignParameters_1.campaignParameters,
// LEAD OPERATIONS - Using modular parameters
...LeadParameters_1.leadParameters,
// ACCOUNT OPERATIONS
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['account'],
},
},
options: [
{
name: 'Get Many Accounts',
value: 'getMany',
description: 'Get many email accounts',
action: 'Get many email accounts',
},
{
name: 'Get Single Account',
value: 'get',
description: 'Get a single email account by email address',
action: 'Get a single email account',
},
],
default: 'getMany',
},
// Email Account Selector
{
displayName: 'Email Account',
name: 'emailAccount',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select an email account...',
typeOptions: {
searchListMethod: 'getEmailAccounts',
searchable: true,
},
},
{
displayName: 'By Email',
name: 'email',
type: 'string',
placeholder: 'e.g. user@example.com',
},
],
displayOptions: {
show: {
resource: ['account'],
operation: ['get'],
},
},
description: 'The email account to retrieve. Choose from the list, or specify an email address.',
},
// Return All for accounts
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['account'],
operation: ['getMany'],
},
},
description: 'Whether to return all results or only up to a given limit',
},
// Limit for accounts
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 50,
displayOptions: {
show: {
resource: ['account'],
operation: ['getMany'],
returnAll: [false],
},
},
description: 'Max number of results to return',
},
// ANALYTICS OPERATIONS
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['analytics'],
},
},
options: [
{
name: 'Get Campaign Analytics',
value: 'getCampaignAnalytics',
description: 'Get analytics for a campaign',
action: 'Get campaign analytics',
},
],
default: 'getCampaignAnalytics',
},
// Return All toggle for analytics
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['analytics'],
operation: ['getCampaignAnalytics'],
},
},
description: 'Whether to return analytics for all campaigns or just the selected campaign',
},
// Campaign Selector for analytics
{
displayName: 'Campaign',
name: 'campaignId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a campaign...',
typeOptions: {
searchListMethod: 'getCampaigns',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 01234567-89ab-cdef-0123-456789abcdef',
},
],
displayOptions: {
show: {
resource: ['analytics'],
operation: ['getCampaignAnalytics'],
returnAll: [false],
},
},
description: 'The campaign to get analytics for. Choose from the list, or specify an ID.',
},
// Date Range Fields for Analytics
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
default: '',
displayOptions: {
show: {
resource: ['analytics'],
operation: ['getCampaignAnalytics'],
},
},
description: 'Start date for analytics data (YYYY-MM-DD format). Leave empty for all-time analytics.',
placeholder: 'e.g. 2024-01-01',
},
{
displayName: 'End Date',
name: 'endDate',
type: 'dateTime',
default: '',
displayOptions: {
show: {
resource: ['analytics'],
operation: ['getCampaignAnalytics'],
},
},
description: 'End date for analytics data (YYYY-MM-DD format). Leave empty for all-time analytics.',
placeholder: 'e.g. 2024-12-31',
},
],
};
this.methods = {
listSearch: {
getCampaigns,
getEmailAccounts,
getLeads,
},
loadOptions: {
getEmailAccountsForSelection,
},
};
}
async execute() {
const items = this.getInputData();
const returnData = [];
// Helper function to extract campaign ID from resourceLocator
const getCampaignId = (i) => {
const campaignLocator = this.getNodeParameter('campaignId', i);
if (typeof campaignLocator === 'string') {
// Backward compatibility - if it's still a string
return campaignLocator;
}
// Extract value from resourceLocator
return campaignLocator.value || campaignLocator;
};
// Helper function to extract email from resourceLocator
const getEmailAccount = (i) => {
const emailLocator = this.getNodeParameter('emailAccount', i);
if (typeof emailLocator === 'string') {
// Backward compatibility - if it's still a string
return emailLocator;
}
// Extract value from resourceLocator
return emailLocator.value || emailLocator;
};
for (let i = 0; i < items.length; i++) {
try {
const resource = this.getNodeParameter('resource', i);
const operation = this.getNodeParameter('operation', i);
let responseData;
if (resource === 'campaign') {
// Use the modular OperationRouter for Campaign operations
responseData = await OperationRouter_1.OperationRouter.execute(this, i, resource, operation);
}
else if (resource === 'lead') {
// Use the modular OperationRouter for Lead operations
responseData = await OperationRouter_1.OperationRouter.execute(this, i, resource, operation);
}
else if (resource === 'account') {
if (operation === 'getMany') {
const returnAll = this.getNodeParameter('returnAll', i, false);
const limit = this.getNodeParameter('limit', i, 50);
// Validate limit doesn't exceed 100
if (limit > 100) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Limit cannot exceed 100. Instantly API has a maximum limit of 100.', { itemIndex: i });
}
if (returnAll) {
// Get all accounts with pagination
const allAccounts = await paginateInstantlyApi(this, '/api/v2/accounts', 'accounts');
responseData = { items: allAccounts };
}
else {
// Get single page with specified limit
const queryParams = { limit };
responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', '/api/v2/accounts', {}, queryParams);
}
}
else if (operation === 'get') {
const emailAccount = getEmailAccount(i);
responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', `/api/v2/accounts/${emailAccount}`);
}
}
else if (resource === 'analytics') {
if (operation === 'getCampaignAnalytics') {
const returnAll = this.getNodeParameter('returnAll', i, false);
// Get date parameters and format them for the API
const startDateInput = this.getNodeParameter('startDate', i, '');
const endDateInput = this.getNodeParameter('endDate', i, '');
// Build query parameters object
const queryParams = {};
// Add date parameters if provided, using the robust date formatter
const formattedStartDate = formatDateForApi(startDateInput);
const formattedEndDate = formatDateForApi(endDateInput);
if (formattedStartDate) {
queryParams.start_date = formattedStartDate;
}
if (formattedEndDate) {
queryParams.end_date = formattedEndDate;
}
if (returnAll) {
// Get analytics for all campaigns with date filtering
responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', '/api/v2/campaigns/analytics', {}, queryParams);
}
else {
// Get analytics for specific campaign with date filtering
const campaignId = getCampaignId(i);
queryParams.id = campaignId;
responseData = await generic_functions_1.instantlyApiRequest.call(this, 'GET', '/api/v2/campaigns/analytics', {}, queryParams);
}
}
}
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
returnData.push(...executionData);
}
catch (error) {
if (this.continueOnFail()) {
const executionData = this.helpers.constructExecutionMetaData([{ json: { error: error.message } }], { itemData: { item: i } });
returnData.push(...executionData);
continue;
}
throw error;
}
}
return [returnData];
}
}
exports.InstantlyApi = InstantlyApi;
//# sourceMappingURL=InstantlyApi.node.js.map