n8n-nodes-salla
Version:
n8n node for Salla.sa e-commerce platform integration with OAuth2.0
256 lines (255 loc) • 10.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sallaApiRequest = sallaApiRequest;
exports.sallaApiRequestAllItems = sallaApiRequestAllItems;
exports.validateWebhook = validateWebhook;
exports.formatSallaDate = formatSallaDate;
exports.parseSallaFilters = parseSallaFilters;
const n8n_workflow_1 = require("n8n-workflow");
const moment_1 = __importDefault(require("moment"));
async function sallaApiRequest(method, endpoint, body, qs, optionsOverride) {
var _a, _b, _c, _d, _e;
const credentials = await this.getCredentials('sallaOAuth2Api');
if (!credentials) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No credentials found!');
}
const environment = credentials.environment;
const apiVersion = credentials.apiVersion;
const rateLimitHandling = credentials.rateLimitHandling;
const maxRetries = credentials.maxRetries || 3;
const autoRefreshTokens = (_a = credentials.autoRefreshTokens) !== null && _a !== void 0 ? _a : true;
const tokenRefreshBuffer = credentials.tokenRefreshBuffer || 30;
// Base URL based on environment
const baseURL = environment === 'sandbox'
? `https://api.s-cart.com/admin/${apiVersion}`
: `https://api.salla.dev/admin/${apiVersion}`;
// Check if this is a formData request (multipart/form-data)
const isFormData = (optionsOverride === null || optionsOverride === void 0 ? void 0 : optionsOverride.formData) !== undefined;
const options = {
method,
url: `${baseURL}${endpoint}`,
headers: {
'Accept': 'application/json',
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...optionsOverride === null || optionsOverride === void 0 ? void 0 : optionsOverride.headers,
},
json: true,
};
// Handle formData for file uploads (multipart/form-data)
if (isFormData && (optionsOverride === null || optionsOverride === void 0 ? void 0 : optionsOverride.formData)) {
options.formData = optionsOverride.formData;
// Don't send body when using formData
}
else if (body && Object.keys(body).length > 0) {
options.body = body;
}
if (qs && Object.keys(qs).length > 0) {
options.qs = qs;
}
let attempts = 0;
const maxAttempts = rateLimitHandling === 'retry' ? maxRetries + 1 : 1;
while (attempts < maxAttempts) {
try {
// Check if token needs refresh (if auto refresh is enabled)
if (autoRefreshTokens) {
await checkAndRefreshToken.call(this, tokenRefreshBuffer);
}
const response = await this.helpers.requestOAuth2.call(this, 'sallaOAuth2Api', options);
return response;
}
catch (error) {
attempts++;
// Handle rate limiting
if (error.httpCode === '429' && rateLimitHandling === 'retry' && attempts < maxAttempts) {
const retryAfter = ((_c = (_b = error.response) === null || _b === void 0 ? void 0 : _b.headers) === null || _c === void 0 ? void 0 : _c['retry-after']) || ((_e = (_d = error.response) === null || _d === void 0 ? void 0 : _d.headers) === null || _e === void 0 ? void 0 : _e['x-ratelimit-reset']);
let waitTime = 60; // Default wait time in seconds
if (retryAfter) {
// If retry-after is a timestamp
if (retryAfter.length > 10) {
waitTime = Math.max(moment_1.default.unix(parseInt(retryAfter)).diff((0, moment_1.default)(), 'seconds'), 1);
}
else {
waitTime = parseInt(retryAfter);
}
}
// Use setTimeout in a way that works in both browser and Node.js environments
await new Promise(resolve => {
if (typeof setTimeout !== 'undefined') {
setTimeout(resolve, waitTime * 1000);
}
else {
// Fallback for environments where setTimeout might not be available
const start = Date.now();
while (Date.now() - start < waitTime * 1000) {
// Busy wait (not ideal, but works as fallback)
}
resolve(undefined);
}
});
continue;
}
// Handle OAuth errors by attempting token refresh
if (error.httpCode === '401' && attempts === 1 && autoRefreshTokens) {
try {
await refreshAccessToken.call(this);
continue; // Retry with new token
}
catch (refreshError) {
throw new n8n_workflow_1.NodeApiError(this.getNode(), refreshError);
}
}
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
}
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Max retry attempts reached');
}
async function sallaApiRequestAllItems(method, endpoint, body, query) {
const returnData = [];
let page = 1;
const perPage = 100;
do {
const qs = {
...query,
page,
per_page: perPage,
};
const responseData = await sallaApiRequest.call(this, method, endpoint, body, qs);
if (responseData.data && Array.isArray(responseData.data)) {
returnData.push(...responseData.data);
}
else if (Array.isArray(responseData)) {
returnData.push(...responseData);
}
else {
break;
}
// Check if there are more pages
if (responseData.pagination) {
if (responseData.pagination.current_page >= responseData.pagination.last_page) {
break;
}
}
else if (responseData.data && responseData.data.length < perPage) {
break;
}
page++;
} while (true);
return returnData;
}
async function checkAndRefreshToken(refreshBuffer = 30) {
const credentials = await this.getCredentials('sallaOAuth2Api');
if (!credentials.oauthTokenData) {
return; // No token data available
}
const tokenData = credentials.oauthTokenData;
const expiresAt = tokenData.expires_at;
if (!expiresAt) {
return; // No expiration data
}
const expirationTime = (0, moment_1.default)(expiresAt);
const now = (0, moment_1.default)();
const minutesUntilExpiry = expirationTime.diff(now, 'minutes');
// Refresh token if it expires within the specified buffer time
if (minutesUntilExpiry < refreshBuffer) {
await refreshAccessToken.call(this);
}
}
async function refreshAccessToken() {
const credentials = await this.getCredentials('sallaOAuth2Api');
if (!credentials.oauthTokenData) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No OAuth token data found');
}
const tokenData = credentials.oauthTokenData;
const refreshToken = tokenData.refresh_token;
if (!refreshToken) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No refresh token found');
}
const environment = credentials.environment;
const clientId = credentials.clientId;
const clientSecret = credentials.clientSecret;
const tokenUrl = environment === 'sandbox'
? 'https://accounts.s-cart.com/oauth2/token'
: 'https://accounts.salla.sa/oauth2/token';
const options = {
method: 'POST',
url: tokenUrl,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
form: {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret,
},
json: true,
};
try {
const response = await this.helpers.request(options);
// Update the credentials with new token data
const newTokenData = {
...tokenData,
access_token: response.access_token,
refresh_token: response.refresh_token || refreshToken,
expires_in: response.expires_in,
expires_at: (0, moment_1.default)().add(response.expires_in, 'seconds').toISOString(),
obtained_at: new Date().toISOString(),
environment: environment,
};
// Save the updated credentials - this should trigger n8n to update the stored credentials
const updatedCredentials = {
...credentials,
oauthTokenData: newTokenData,
};
// Note: n8n should handle saving the updated credentials automatically
// The requestOAuth2 helper should use the updated token for subsequent requests
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to refresh access token: ${error.message || 'Unknown error'}`);
}
}
function validateWebhook(body, signature, secret) {
// Use require in a way that works in both Node.js and browser environments
let crypto;
try {
// Try to require crypto module (Node.js environment)
crypto = require('crypto');
}
catch (error) {
// Fallback for browser environment or when crypto is not available
throw new Error('Crypto module not available for webhook validation');
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return `sha256=${expectedSignature}` === signature;
}
function formatSallaDate(date) {
return (0, moment_1.default)(date).format('YYYY-MM-DD HH:mm:ss');
}
function parseSallaFilters(filters) {
const queryParams = {};
if (filters.status) {
if (Array.isArray(filters.status)) {
queryParams.status = filters.status.join(',');
}
else {
queryParams.status = filters.status;
}
}
if (filters.dateFrom) {
queryParams.created_from = formatSallaDate(filters.dateFrom);
}
if (filters.dateTo) {
queryParams.created_to = formatSallaDate(filters.dateTo);
}
if (filters.search) {
queryParams.search = filters.search;
}
return queryParams;
}