UNPKG

n8n-nodes-salla

Version:

n8n node for Salla.sa e-commerce platform integration with OAuth2.0

256 lines (255 loc) 10.6 kB
"use strict"; 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; }