UNPKG

n8n-nodes-salla1

Version:

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

279 lines (278 loc) 11.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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 = __importStar(require("moment")); async function sallaApiRequest(method, endpoint, body, qs, headers) { 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}`; const options = { method, url: `${baseURL}${endpoint}`, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...headers, }, json: true, }; 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.unix(parseInt(retryAfter)).diff(moment(), '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 = moment(expiresAt); const now = moment(); 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: moment().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 moment(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; }