elnk-pro-link-shortener
Version:
A comprehensive Node.js library for creating and managing short URLs using the elnk.pro API with enhanced features including link management, statistics, and bulk operations
907 lines (793 loc) • 30.1 kB
JavaScript
const axios = require('axios');
/**
* ElnkProShortener - A Node.js library for creating short URLs using the elnk.pro API
*
* @class ElnkProShortener
*/
class ElnkProShortener {
/**
* Create an instance of ElnkProShortener
*
* @param {Object} config - Configuration object
* @param {string} config.apiKey - The elnk.pro API key (required)
* @param {string} [config.domainId] - The domain ID (optional)
* @param {string} [config.projectId] - The project ID (optional)
* @param {number} [config.timeout=30000] - Request timeout in milliseconds
* @param {string} [config.baseURL='https://elnk.pro/api'] - Base API URL
*/
constructor(config = {}) {
if (!config.apiKey) {
throw new Error('API key is required');
}
this.apiKey = config.apiKey;
this.domainId = config.domainId || null;
this.projectId = config.projectId || null;
this.timeout = config.timeout || 30000;
this.baseURL = config.baseURL || 'https://elnk.pro/api';
// Configure axios instance
this.client = axios.create({
baseURL: this.baseURL,
timeout: this.timeout,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Accept': 'application/json'
}
});
}
/**
* Create a short URL
*
* @param {string} originalUrl - The original URL to shorten
* @param {string} [customAlias] - Custom alias for the short URL
* @returns {Promise<Object>} Response object with success status and data
*/
async createShortUrl(originalUrl, customAlias = null) {
if (!originalUrl) {
throw new Error('Original URL is required');
}
try {
const data = {
destination: originalUrl,
type: 'link'
};
// Add optional fields if available
if (this.domainId) {
data.domain_id = this.domainId;
}
if (this.projectId) {
data.project_id = this.projectId;
}
if (customAlias) {
data.url = customAlias;
} else {
data.url = null; // Let API generate random alias
}
// Remove null values
Object.keys(data).forEach(key => {
if (data[key] === null) {
delete data[key];
}
});
const response = await this.client.post('/links', data);
if (response.status >= 200 && response.status < 300) {
const linkId = response.data.data?.id || response.data.id;
if (linkId) {
// Get link details to construct the full short URL
const linkDetails = await this.getLinkDetails(linkId);
if (linkDetails.success) {
const shortUrl = this.constructShortUrl(linkDetails.data);
return {
success: true,
data: {
id: linkId,
originalUrl: originalUrl,
shortUrl: shortUrl,
customAlias: customAlias,
clicks: linkDetails.data.clicks || 0,
createdAt: linkDetails.data.created_at || new Date().toISOString()
}
};
}
}
return {
success: true,
data: response.data
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Create multiple short URLs in bulk
*
* @param {Array<string>} urls - Array of URLs to shorten
* @param {string} [baseAlias] - Base alias for URLs (will be suffixed with numbers)
* @returns {Promise<Object>} Response object with success status and results
*/
async createBulkShortUrls(urls, baseAlias = null) {
if (!Array.isArray(urls) || urls.length === 0) {
throw new Error('URLs array is required and cannot be empty');
}
const results = [];
const errors = [];
for (let i = 0; i < urls.length; i++) {
try {
const url = urls[i].trim();
if (!url) continue;
let customAlias = null;
if (baseAlias) {
customAlias = urls.length > 1 ? `${baseAlias}-${i + 1}` : baseAlias;
}
const result = await this.createShortUrl(url, customAlias);
if (result.success) {
results.push(result.data);
} else {
errors.push({
url: url,
error: result.message
});
}
} catch (error) {
errors.push({
url: urls[i],
error: error.message
});
}
}
return {
success: errors.length === 0,
data: {
successful: results,
failed: errors,
total: urls.length,
successCount: results.length,
errorCount: errors.length
}
};
}
/**
* Get details of a short link
*
* @param {string} linkId - The ID of the link
* @returns {Promise<Object>} Response object with success status and link details
*/
async getLinkDetails(linkId) {
if (!linkId) {
throw new Error('Link ID is required');
}
try {
const response = await this.client.get(`/links/${linkId}`);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Get domain details
*
* @param {string} domainId - The ID of the domain
* @returns {Promise<Object>} Response object with success status and domain details
*/
async getDomainDetails(domainId) {
if (!domainId) {
throw new Error('Domain ID is required');
}
try {
const response = await this.client.get(`/domains/${domainId}`);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Delete a short link
*
* @param {string} linkId - The ID of the link to delete
* @returns {Promise<Object>} Response object with success status
*/
async deleteLink(linkId) {
if (!linkId) {
throw new Error('Link ID is required');
}
try {
const response = await this.client.delete(`/links/${linkId}`);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
message: 'Link deleted successfully'
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Update an existing short link
*
* @param {string} linkId - The ID of the link to update
* @param {Object} updateData - Data to update
* @param {string} [updateData.destination] - New destination URL
* @param {string} [updateData.url] - New custom alias
* @param {string} [updateData.title] - Link title
* @param {string} [updateData.description] - Link description
* @returns {Promise<Object>} Response object with success status and updated data
*/
async updateLink(linkId, updateData) {
if (!linkId) {
throw new Error('Link ID is required');
}
if (!updateData || Object.keys(updateData).length === 0) {
throw new Error('Update data is required');
}
try {
const response = await this.client.put(`/links/${linkId}`, updateData);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data || response.data
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Get click statistics for a link
*
* @param {string} linkId - The ID of the link
* @param {Object} [options] - Query options
* @param {string} [options.period] - Time period (day, week, month, year, all)
* @param {string} [options.timezone] - Timezone for statistics
* @returns {Promise<Object>} Response object with success status and statistics
*/
async getLinkStats(linkId, options = {}) {
if (!linkId) {
throw new Error('Link ID is required');
}
try {
const params = new URLSearchParams();
if (options.period) {
params.append('period', options.period);
}
if (options.timezone) {
params.append('timezone', options.timezone);
}
const queryString = params.toString();
const url = `/links/${linkId}/stats${queryString ? `?${queryString}` : ''}`;
const response = await this.client.get(url);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data || response.data
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Get list of all user's links
*
* @param {Object} [options] - Query options
* @param {number} [options.page=1] - Page number for pagination
* @param {number} [options.limit=25] - Number of results per page
* @param {string} [options.search] - Search term
* @param {string} [options.sort] - Sort field (created_at, clicks, title)
* @param {string} [options.order] - Sort order (asc, desc)
* @returns {Promise<Object>} Response object with success status and links list
*/
async getAllLinks(options = {}) {
try {
const params = new URLSearchParams();
params.append('page', options.page || 1);
params.append('limit', options.limit || 25);
if (options.search) {
params.append('search', options.search);
}
if (options.sort) {
params.append('sort', options.sort);
}
if (options.order) {
params.append('order', options.order);
}
const response = await this.client.get(`/links?${params.toString()}`);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data || response.data,
pagination: response.data.pagination || null
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Get list of user's domains
*
* @returns {Promise<Object>} Response object with success status and domains list
*/
async getDomains() {
try {
const response = await this.client.get('/domains');
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data || response.data
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Search links by various criteria
*
* @param {Object} searchOptions - Search criteria
* @param {string} [searchOptions.query] - Search query
* @param {string} [searchOptions.domain] - Filter by domain
* @param {string} [searchOptions.tag] - Filter by tag
* @param {Date} [searchOptions.dateFrom] - Filter from date
* @param {Date} [searchOptions.dateTo] - Filter to date
* @param {number} [searchOptions.page=1] - Page number
* @param {number} [searchOptions.limit=25] - Results per page
* @returns {Promise<Object>} Response object with search results
*/
async searchLinks(searchOptions = {}) {
try {
const params = new URLSearchParams();
if (searchOptions.query) {
params.append('q', searchOptions.query);
}
if (searchOptions.domain) {
params.append('domain', searchOptions.domain);
}
if (searchOptions.tag) {
params.append('tag', searchOptions.tag);
}
if (searchOptions.dateFrom) {
params.append('date_from', searchOptions.dateFrom.toISOString().split('T')[0]);
}
if (searchOptions.dateTo) {
params.append('date_to', searchOptions.dateTo.toISOString().split('T')[0]);
}
params.append('page', searchOptions.page || 1);
params.append('limit', searchOptions.limit || 25);
const response = await this.client.get(`/links/search?${params.toString()}`);
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data || response.data,
pagination: response.data.pagination || null
};
} else {
return {
success: false,
message: response.data.message || `API request failed with status code: ${response.status}`
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Construct the full short URL from link data
*
* @param {Object} linkData - Link data from API
* @returns {string|false} The constructed short URL or false if failed
*/
constructShortUrl(linkData) {
const urlSlug = linkData.url;
const domainId = linkData.domain_id;
if (!urlSlug) {
return false;
}
// If domain_id is 0 or not set, use default elnk.pro domain
if (!domainId || domainId === 0) {
return `https://elnk.pro/${urlSlug}`;
}
// For custom domains, we would need to fetch domain details
// For now, return with default domain
return `https://elnk.pro/${urlSlug}`;
}
/**
* Handle API errors
*
* @param {Error} error - The error object
* @returns {Object} Formatted error response
*/
handleError(error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
return {
success: false,
message: error.response.data?.message || `API request failed with status code: ${error.response.status}`,
statusCode: error.response.status
};
} else if (error.request) {
// The request was made but no response was received
return {
success: false,
message: 'No response received from API server'
};
} else {
// Something happened in setting up the request that triggered an Error
return {
success: false,
message: error.message || 'Unknown error occurred'
};
}
}
/**
* Validate URL format with enhanced options
*
* @param {string} url - URL to validate
* @param {Object} [options] - Validation options
* @param {boolean} [options.requireProtocol=true] - Require http/https protocol
* @param {boolean} [options.allowLocalhost=true] - Allow localhost URLs
* @param {boolean} [options.allowIP=true] - Allow IP addresses
* @param {Array<string>} [options.allowedProtocols=['http', 'https']] - Allowed protocols
* @returns {Object} Validation result with success status and details
*/
static validateUrl(url, options = {}) {
const defaultOptions = {
requireProtocol: true,
allowLocalhost: true,
allowIP: true,
allowedProtocols: ['http', 'https']
};
const opts = { ...defaultOptions, ...options };
if (!url || typeof url !== 'string') {
return {
valid: false,
error: 'URL must be a non-empty string'
};
}
try {
const parsedUrl = new URL(url);
// Check protocol
if (opts.requireProtocol && !opts.allowedProtocols.includes(parsedUrl.protocol.slice(0, -1))) {
return {
valid: false,
error: `Protocol must be one of: ${opts.allowedProtocols.join(', ')}`
};
}
// Check localhost
if (!opts.allowLocalhost && (parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1')) {
return {
valid: false,
error: 'Localhost URLs are not allowed'
};
}
// Check IP addresses
if (!opts.allowIP && /^\d+\.\d+\.\d+\.\d+$/.test(parsedUrl.hostname)) {
return {
valid: false,
error: 'IP addresses are not allowed'
};
}
// Check for valid hostname
if (!parsedUrl.hostname || parsedUrl.hostname.length === 0) {
return {
valid: false,
error: 'URL must have a valid hostname'
};
}
return {
valid: true,
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
pathname: parsedUrl.pathname,
search: parsedUrl.search
};
} catch (error) {
return {
valid: false,
error: 'Invalid URL format'
};
}
}
/**
* Validate URL format (simple version for backward compatibility)
*
* @param {string} url - URL to validate
* @returns {boolean} True if valid URL
*/
static isValidUrl(url) {
const result = ElnkProShortener.validateUrl(url);
return result.valid;
}
/**
* Set API key
*
* @param {string} apiKey - New API key
*/
setApiKey(apiKey) {
if (!apiKey) {
throw new Error('API key is required');
}
this.apiKey = apiKey;
this.client.defaults.headers['Authorization'] = `Bearer ${apiKey}`;
}
/**
* Set domain ID
*
* @param {string} domainId - Domain ID
*/
setDomainId(domainId) {
this.domainId = domainId;
}
/**
* Set project ID
*
* @param {string} projectId - Project ID
*/
setProjectId(projectId) {
this.projectId = projectId;
}
/**
* Get current configuration
*
* @returns {Object} Current configuration (excluding API key for security)
*/
getConfig() {
return {
domainId: this.domainId,
projectId: this.projectId,
timeout: this.timeout,
baseURL: this.baseURL
};
}
/**
* Test API connection and authentication
*
* @returns {Promise<Object>} Connection test result
*/
async testConnection() {
try {
const response = await this.client.get('/user');
if (response.status >= 200 && response.status < 300) {
return {
success: true,
message: 'Connection successful',
user: response.data.data || response.data
};
} else {
return {
success: false,
message: 'Authentication failed'
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Get account/user information
*
* @returns {Promise<Object>} User account details
*/
async getUserInfo() {
try {
const response = await this.client.get('/user');
if (response.status >= 200 && response.status < 300) {
return {
success: true,
data: response.data.data || response.data
};
} else {
return {
success: false,
message: response.data.message || 'Failed to get user information'
};
}
} catch (error) {
return this.handleError(error);
}
}
/**
* Find a link by its short URL
*
* @param {string} shortUrl - The short URL to find
* @returns {Promise<Object>} Link details if found
*/
async findLinkByUrl(shortUrl) {
try {
// Extract the slug from the short URL
const url = new URL(shortUrl);
const slug = url.pathname.substring(1); // Remove leading slash
if (!slug) {
return {
success: false,
message: 'Invalid short URL format'
};
}
// Search for the link
const searchResult = await this.searchLinks({ query: slug });
if (searchResult.success && searchResult.data.length > 0) {
// Find exact match
const exactMatch = searchResult.data.find(link =>
link.url === slug || this.constructShortUrl(link) === shortUrl
);
if (exactMatch) {
return {
success: true,
data: exactMatch
};
}
}
return {
success: false,
message: 'Link not found'
};
} catch (error) {
return this.handleError(error);
}
}
/**
* Bulk delete multiple links
*
* @param {Array<string>} linkIds - Array of link IDs to delete
* @returns {Promise<Object>} Bulk deletion results
*/
async bulkDeleteLinks(linkIds) {
if (!Array.isArray(linkIds) || linkIds.length === 0) {
throw new Error('Link IDs array is required and cannot be empty');
}
const results = [];
const errors = [];
for (const linkId of linkIds) {
try {
const result = await this.deleteLink(linkId);
if (result.success) {
results.push({ linkId, success: true });
} else {
errors.push({ linkId, error: result.message });
}
} catch (error) {
errors.push({ linkId, error: error.message });
}
}
return {
success: errors.length === 0,
data: {
successful: results,
failed: errors,
total: linkIds.length,
successCount: results.length,
errorCount: errors.length
}
};
}
/**
* Generate a random alias
*
* @param {number} [length=8] - Length of the alias
* @param {boolean} [includeNumbers=true] - Include numbers
* @param {boolean} [includeSpecialChars=false] - Include special characters
* @returns {string} Generated alias
*/
static generateAlias(length = 8, includeNumbers = true, includeSpecialChars = false) {
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (includeNumbers) {
chars += '0123456789';
}
if (includeSpecialChars) {
chars += '-_';
}
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Format bytes to human readable format
*
* @param {number} bytes - Bytes to format
* @param {number} [decimals=2] - Number of decimal places
* @returns {string} Formatted size
*/
static formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* Create a short URL with retry logic
*
* @param {string} originalUrl - The original URL to shorten
* @param {string} [customAlias] - Custom alias for the short URL
* @param {Object} [options] - Retry options
* @param {number} [options.maxRetries=3] - Maximum number of retries
* @param {number} [options.retryDelay=1000] - Delay between retries in ms
* @returns {Promise<Object>} Response object with success status and data
*/
async createShortUrlWithRetry(originalUrl, customAlias = null, options = {}) {
const { maxRetries = 3, retryDelay = 1000 } = options;
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await this.createShortUrl(originalUrl, customAlias);
if (result.success) {
return result;
}
lastError = result;
// If it's not a server error, don't retry
if (result.statusCode && result.statusCode < 500) {
break;
}
// Wait before retrying
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
}
} catch (error) {
lastError = this.handleError(error);
// Wait before retrying
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
}
}
}
return lastError || {
success: false,
message: 'Failed to create short URL after multiple attempts'
};
}
}
module.exports = ElnkProShortener;