facebookApiService.js•16.3 kB
/**
* Facebook API Service
* Handles interactions with the Facebook Marketing API
*/
const axios = require('axios');
const { URLSearchParams } = require('url');
const { FacebookApiError } = require('../utils/errorTypes');
const tokenManager = require('../utils/tokenManager');
const facebookConfig = require('../config/facebook');
const logger = require('../utils/logger');
/**
* Facebook API Service class
*/
class FacebookApiService {
/**
* Create a new Facebook API Service instance
* @param {string} accessToken - Facebook access token
*/
constructor(accessToken) {
this.accessToken = accessToken;
this.baseUrl = `${facebookConfig.endpoints.base}/${facebookConfig.apiVersion}`;
this.requestDefaults = facebookConfig.requestDefaults;
}
/**
* Make a GET request to the Facebook API
* @param {string} endpoint - API endpoint
* @param {Object} params - Query parameters
* @param {Object} options - Request options
* @returns {Promise<Object>} - API response
*/
async get(endpoint, params = {}, options = {}) {
try {
const url = this._buildUrl(endpoint);
const queryParams = {
access_token: this.accessToken,
...params
};
const config = {
params: queryParams,
timeout: options.timeout || this.requestDefaults.timeout
};
const response = await this._makeRequest(() => axios.get(url, config));
return response.data;
} catch (error) {
throw this._handleApiError(error);
}
}
/**
* Make a POST request to the Facebook API
* @param {string} endpoint - API endpoint
* @param {Object} data - Request body
* @param {Object} params - Query parameters
* @param {Object} options - Request options
* @returns {Promise<Object>} - API response
*/
async post(endpoint, data = {}, params = {}, options = {}) {
try {
const url = this._buildUrl(endpoint);
const queryParams = {
access_token: this.accessToken,
...params
};
const config = {
params: queryParams,
timeout: options.timeout || this.requestDefaults.timeout
};
const response = await this._makeRequest(() => axios.post(url, data, config));
return response.data;
} catch (error) {
throw this._handleApiError(error);
}
}
/**
* Make a DELETE request to the Facebook API
* @param {string} endpoint - API endpoint
* @param {Object} params - Query parameters
* @param {Object} options - Request options
* @returns {Promise<Object>} - API response
*/
async delete(endpoint, params = {}, options = {}) {
try {
const url = this._buildUrl(endpoint);
const queryParams = {
access_token: this.accessToken,
...params
};
const config = {
params: queryParams,
timeout: options.timeout || this.requestDefaults.timeout
};
const response = await this._makeRequest(() => axios.delete(url, config));
return response.data;
} catch (error) {
throw this._handleApiError(error);
}
}
/**
* Make a batch request to the Facebook API
* @param {Array} requests - Array of request objects
* @param {Object} options - Request options
* @returns {Promise<Array>} - Array of API responses
*/
async batch(requests, options = {}) {
try {
const url = `${this.baseUrl}/`;
const batchSize = options.batchSize || this.requestDefaults.maxBatchSize;
// Split requests into batches
const batches = [];
for (let i = 0; i < requests.length; i += batchSize) {
batches.push(requests.slice(i, i + batchSize));
}
// Process each batch
const results = [];
for (const batch of batches) {
const batchRequests = batch.map(request => ({
method: request.method || 'GET',
relative_url: request.relative_url,
body: request.body,
name: request.name
}));
const params = new URLSearchParams();
params.append('access_token', this.accessToken);
params.append('batch', JSON.stringify(batchRequests));
const response = await this._makeRequest(() =>
axios.post(url, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: options.timeout || this.requestDefaults.timeout
})
);
// Process batch response
const batchResults = response.data.map(result => {
if (result === null) {
return { error: { message: 'Batch request failed' } };
}
if (result.code !== 200) {
return {
error: {
message: result.body,
code: result.code
}
};
}
try {
return JSON.parse(result.body);
} catch (e) {
return { body: result.body };
}
});
results.push(...batchResults);
}
return results;
} catch (error) {
throw this._handleApiError(error);
}
}
/**
* Get user ad accounts
* @returns {Promise<Array>} - Array of ad accounts
*/
async getAdAccounts() {
const fields = [
'id',
'name',
'account_id',
'account_status',
'age',
'business_city',
'business_country_code',
'business_name',
'business_street',
'business_street2',
'business_zip',
'currency',
'timezone_name',
'timezone_offset_hours_utc',
'capabilities',
'spend_cap',
'amount_spent',
'balance'
].join(',');
const response = await this.get(facebookConfig.endpoints.adAccounts, {
fields,
limit: 100
});
return response.data || [];
}
/**
* Get campaigns for an ad account
* @param {string} adAccountId - Ad account ID
* @param {Object} params - Query parameters
* @returns {Promise<Array>} - Array of campaigns
*/
async getCampaigns(adAccountId, params = {}) {
const fields = [
'id',
'name',
'objective',
'status',
'special_ad_categories',
'spend_cap',
'daily_budget',
'lifetime_budget',
'start_time',
'stop_time',
'created_time',
'updated_time'
].join(',');
const response = await this.get(`act_${adAccountId}/campaigns`, {
fields,
limit: params.limit || 100,
...params
});
return response.data || [];
}
/**
* Get ad sets for a campaign
* @param {string} campaignId - Campaign ID
* @param {Object} params - Query parameters
* @returns {Promise<Array>} - Array of ad sets
*/
async getAdSets(campaignId, params = {}) {
const fields = [
'id',
'name',
'campaign_id',
'status',
'targeting',
'optimization_goal',
'bid_strategy',
'bid_amount',
'budget_remaining',
'daily_budget',
'lifetime_budget',
'start_time',
'end_time',
'created_time',
'updated_time'
].join(',');
const response = await this.get(`${campaignId}/adsets`, {
fields,
limit: params.limit || 100,
...params
});
return response.data || [];
}
/**
* Get ads for an ad set
* @param {string} adSetId - Ad set ID
* @param {Object} params - Query parameters
* @returns {Promise<Array>} - Array of ads
*/
async getAds(adSetId, params = {}) {
const fields = [
'id',
'name',
'adset_id',
'campaign_id',
'status',
'creative',
'preview_url',
'created_time',
'updated_time'
].join(',');
const response = await this.get(`${adSetId}/ads`, {
fields,
limit: params.limit || 100,
...params
});
return response.data || [];
}
/**
* Get insights for an entity (ad account, campaign, ad set, or ad)
* @param {string} entityId - Entity ID
* @param {string} timeRange - Time range (e.g., 'today', 'yesterday', 'last_7_days')
* @param {Array} metrics - Array of metrics to retrieve
* @param {Object} params - Additional parameters
* @returns {Promise<Array>} - Array of insights
*/
async getInsights(entityId, timeRange, metrics = [], params = {}) {
const defaultMetrics = [
'impressions',
'reach',
'clicks',
'ctr',
'cpc',
'cpm',
'spend',
'frequency',
'conversions',
'cost_per_conversion',
'conversion_rate_ranking',
'quality_ranking',
'engagement_rate_ranking'
];
const fields = metrics.length > 0 ? metrics : defaultMetrics;
const response = await this.get(`${entityId}/insights`, {
fields: fields.join(','),
time_range: this._formatTimeRange(timeRange),
level: params.level || 'ad',
limit: params.limit || 100,
...params
});
return response.data || [];
}
/**
* Create a campaign
* @param {string} adAccountId - Ad account ID
* @param {Object} campaignData - Campaign data
* @returns {Promise<Object>} - Created campaign
*/
async createCampaign(adAccountId, campaignData) {
const response = await this.post(`act_${adAccountId}/campaigns`, campaignData);
return response;
}
/**
* Update a campaign
* @param {string} campaignId - Campaign ID
* @param {Object} campaignData - Campaign data
* @returns {Promise<Object>} - Updated campaign
*/
async updateCampaign(campaignId, campaignData) {
const response = await this.post(campaignId, campaignData);
return response;
}
/**
* Delete a campaign
* @param {string} campaignId - Campaign ID
* @returns {Promise<Object>} - Deletion response
*/
async deleteCampaign(campaignId) {
const response = await this.delete(campaignId);
return response;
}
/**
* Create an ad set
* @param {string} adAccountId - Ad account ID
* @param {Object} adSetData - Ad set data
* @returns {Promise<Object>} - Created ad set
*/
async createAdSet(adAccountId, adSetData) {
const response = await this.post(`act_${adAccountId}/adsets`, adSetData);
return response;
}
/**
* Update an ad set
* @param {string} adSetId - Ad set ID
* @param {Object} adSetData - Ad set data
* @returns {Promise<Object>} - Updated ad set
*/
async updateAdSet(adSetId, adSetData) {
const response = await this.post(adSetId, adSetData);
return response;
}
/**
* Delete an ad set
* @param {string} adSetId - Ad set ID
* @returns {Promise<Object>} - Deletion response
*/
async deleteAdSet(adSetId) {
const response = await this.delete(adSetId);
return response;
}
/**
* Create an ad
* @param {string} adAccountId - Ad account ID
* @param {Object} adData - Ad data
* @returns {Promise<Object>} - Created ad
*/
async createAd(adAccountId, adData) {
const response = await this.post(`act_${adAccountId}/ads`, adData);
return response;
}
/**
* Update an ad
* @param {string} adId - Ad ID
* @param {Object} adData - Ad data
* @returns {Promise<Object>} - Updated ad
*/
async updateAd(adId, adData) {
const response = await this.post(adId, adData);
return response;
}
/**
* Delete an ad
* @param {string} adId - Ad ID
* @returns {Promise<Object>} - Deletion response
*/
async deleteAd(adId) {
const response = await this.delete(adId);
return response;
}
/**
* Build a URL for the Facebook API
* @param {string} endpoint - API endpoint
* @returns {string} - Full URL
* @private
*/
_buildUrl(endpoint) {
// Remove leading slash if present
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
return `${this.baseUrl}/${cleanEndpoint}`;
}
/**
* Make a request with retry logic
* @param {Function} requestFn - Request function
* @returns {Promise<Object>} - API response
* @private
*/
async _makeRequest(requestFn) {
let retries = 0;
const maxRetries = this.requestDefaults.retries;
const retryDelay = this.requestDefaults.retryDelay;
while (true) {
try {
return await requestFn();
} catch (error) {
// Don't retry if we've reached the maximum number of retries
if (retries >= maxRetries) {
throw error;
}
// Don't retry for certain error types
if (error.response) {
const status = error.response.status;
// Don't retry for client errors (except rate limiting)
if (status >= 400 && status < 500 && status !== 429) {
throw error;
}
}
// Increment retry count
retries++;
// Calculate delay with exponential backoff
const delay = retryDelay * Math.pow(2, retries - 1);
// Log retry attempt
logger.warn(`Retrying Facebook API request (${retries}/${maxRetries}) after ${delay}ms`);
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/**
* Handle API errors
* @param {Error} error - API error
* @returns {Error} - Formatted error
* @private
*/
_handleApiError(error) {
let message = 'Facebook API error';
let statusCode = 500;
let fbErrorCode = null;
let fbErrorSubcode = null;
if (error.response) {
statusCode = error.response.status;
if (error.response.data && error.response.data.error) {
const fbError = error.response.data.error;
message = fbError.message || message;
fbErrorCode = fbError.code;
fbErrorSubcode = fbError.error_subcode;
logger.error(`Facebook API error: ${message} (Code: ${fbErrorCode}, Subcode: ${fbErrorSubcode})`);
}
} else if (error.request) {
message = 'No response received from Facebook API';
logger.error(`Facebook API request error: ${message}`);
} else {
message = error.message;
logger.error(`Facebook API error: ${message}`);
}
return new FacebookApiError(message, statusCode, fbErrorCode, fbErrorSubcode);
}
/**
* Format time range for insights API
* @param {string|Object} timeRange - Time range string or object
* @returns {Object} - Formatted time range
* @private
*/
_formatTimeRange(timeRange) {
if (typeof timeRange === 'object') {
return timeRange;
}
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
switch (timeRange) {
case 'today':
return {
since: today.toISOString().split('T')[0],
until: now.toISOString().split('T')[0]
};
case 'yesterday': {
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
return {
since: yesterday.toISOString().split('T')[0],
until: yesterday.toISOString().split('T')[0]
};
}
case 'last_7_days': {
const lastWeek = new Date(today);
lastWeek.setDate(lastWeek.getDate() - 7);
return {
since: lastWeek.toISOString().split('T')[0],
until: today.toISOString().split('T')[0]
};
}
case 'last_30_days': {
const lastMonth = new Date(today);
lastMonth.setDate(lastMonth.getDate() - 30);
return {
since: lastMonth.toISOString().split('T')[0],
until: today.toISOString().split('T')[0]
};
}
default:
return {
since: today.toISOString().split('T')[0],
until: today.toISOString().split('T')[0]
};
}
}
}
/**
* Create a Facebook API Service instance for a user
* @param {Object} user - User object
* @returns {Promise<FacebookApiService>} - Facebook API Service instance
*/
const createForUser = async (user) => {
if (!user || !user.accessToken) {
throw new Error('User access token is required');
}
const accessToken = await tokenManager.decryptToken(user.accessToken);
return new FacebookApiService(accessToken);
};
module.exports = {
FacebookApiService,
createForUser
};