UNPKG

amazon-modern-widgets

Version:

Amazon Modern Widgets for Amazon affiliate websites based on Amazon PAAPI v5

340 lines 12.1 kB
"use strict"; /** * API Implementation. * ---------------------------------------------- * Amazon Modern Widgets (AMW). * * @author : Ludovic Toinel <ludovic@toinel.com> * @src : https://github.com/ltoinel/amw */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AmwApi = void 0; // Lets import our required libraries const config_1 = __importDefault(require("config")); const path_1 = __importDefault(require("path")); const Paapi_1 = require("./Paapi"); const ConfigLog4j_1 = require("../utils/ConfigLog4j"); // Custom error classes class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; } } /** * API Implementation. */ class AmwApi { /** * Main AmwApi constructor. */ constructor(cache) { // Initialize the logger this.log = (0, ConfigLog4j_1.getLogger)("AmwApi"); // The cache to optimize the API calls to Amazon this.cache = cache; // Initialize the Paapi client this.paapi = new Paapi_1.Paapi(); // Get TTL configuration with fallback this.ttl = Number(config_1.default.get('Redis.expire')) || 3600; // 1 hour default // Validate configuration this.validateConfiguration(); this.log.info("AmwApi initialized successfully"); } /** * Validate configuration settings */ validateConfiguration() { if (!AmwApi.PROJECT_DIR) { throw new Error('Server.projectDir configuration is required'); } if (this.ttl <= 0) { throw new Error('Redis.expire must be a positive number'); } } /** * Set the API Search endpoint. * * @param req The request object. * @param res The response object. */ setProductEndpoint(req, res) { return __awaiter(this, void 0, void 0, function* () { try { // Validate and extract parameters const params = this.validateAndExtractParams(req); // Log the request this.logRequest(req, params); // Handle product search by ID or keyword if (params.id) { yield this.handleProductById(params.id, req, res); } else if (params.keyword) { yield this.handleProductByKeyword(params.keyword, req, res); } } catch (error) { this.handleError(error, req, res); } }); } /** * Validate and extract request parameters */ validateAndExtractParams(req) { const { id, keyword } = req.query; // Check if at least one parameter is provided if (!id && !keyword) { throw new ValidationError('Missing required parameter: id or keyword'); } // Validate parameter types if (id && typeof id !== 'string') { throw new ValidationError('Parameter id must be a string'); } if (keyword && typeof keyword !== 'string') { throw new ValidationError('Parameter keyword must be a string'); } // Validate parameter lengths if (id && id.length > AmwApi.MAX_ID_LENGTH) { throw new ValidationError(`ID too long (max ${AmwApi.MAX_ID_LENGTH} characters)`); } if (keyword && keyword.length > AmwApi.MAX_KEYWORD_LENGTH) { throw new ValidationError(`Keyword too long (max ${AmwApi.MAX_KEYWORD_LENGTH} characters)`); } // Sanitize parameters const sanitizedParams = {}; if (id) { sanitizedParams.id = this.sanitizeInput(id); } if (keyword) { sanitizedParams.keyword = this.sanitizeInput(keyword); } return sanitizedParams; } /** * Sanitize input to prevent injection attacks */ sanitizeInput(input) { return input.trim().replace(/[<>"']/g, ''); } /** * Log the incoming request */ logRequest(req, params) { var _a; const ip = ((_a = req.headers['x-forwarded-for']) === null || _a === void 0 ? void 0 : _a.toString().split(',')[0].trim()) || req.ip || 'unknown'; const referer = req.get('referer') || 'none'; this.log.info(`GET /product | id=${params.id || 'none'} | keyword=${params.keyword || 'none'} | IP=${ip} | Referer=${referer}`); } /** * Handle product search by ID */ handleProductById(id, req, res) { return __awaiter(this, void 0, void 0, function* () { const cacheKey = this.generateCacheKey('id', id); // Check cache first const cachedProduct = yield this.findInCache(cacheKey); if (cachedProduct) { this.log.info(`Product found in cache: ${id}`); this.sendSuccessResponse(res, cachedProduct); return; } // Fetch from Amazon API try { const product = yield Promise.race([ this.paapi.getItemApi(id), this.createTimeout(AmwApi.REQUEST_TIMEOUT) ]); yield this.handleApiResponse(cacheKey, product, res); } catch (error) { this.log.error(`Error fetching product by ID ${id}:`, error); throw error; } }); } /** * Handle product search by keyword */ handleProductByKeyword(keyword, req, res) { return __awaiter(this, void 0, void 0, function* () { const cacheKey = this.generateCacheKey('keyword', keyword); // Check cache first const cachedProduct = yield this.findInCache(cacheKey); if (cachedProduct) { this.log.info(`Product found in cache: ${keyword}`); this.sendSuccessResponse(res, cachedProduct); return; } // Fetch from Amazon API try { const product = yield Promise.race([ this.paapi.searchItemApi(keyword), this.createTimeout(AmwApi.REQUEST_TIMEOUT) ]); yield this.handleApiResponse(cacheKey, product, res); } catch (error) { this.log.error(`Error searching product by keyword ${keyword}:`, error); throw error; } }); } /** * Handle API response and caching */ handleApiResponse(cacheKey, product, res) { return __awaiter(this, void 0, void 0, function* () { if (product && product !== null && product !== undefined) { // Save to cache yield this.saveInCache(cacheKey, product); // Send response this.sendSuccessResponse(res, product); } else { this.sendNotFoundResponse(res, cacheKey); } }); } /** * Generate cache key with prefix */ generateCacheKey(type, value) { return `${AmwApi.CACHE_KEY_PREFIX}${type}:${value}`; } /** * Create timeout promise for API requests */ createTimeout(ms) { return new Promise((_, reject) => { setTimeout(() => reject(new Error(`Request timeout after ${ms}ms`)), ms); }); } /** * Send successful response with product data */ sendSuccessResponse(res, product) { res.status(200).json(product); } /** * Send not found response */ sendNotFoundResponse(res, identifier) { this.log.info(`Product not found in Amazon: ${identifier}`); res.status(404).json({ error: "Product Not Found", message: `No product found for identifier: ${identifier}`, timestamp: new Date().toISOString() }); } /** * Handle errors and send appropriate response */ handleError(error, req, res) { const err = error instanceof Error ? error : new Error(String(error)); this.log.error(`API Error: ${err.message}`, err); if (error instanceof ValidationError) { res.status(400).json({ error: "Validation Error", message: err.message, timestamp: new Date().toISOString() }); } else if (err.message.includes('timeout')) { res.status(504).json({ error: "Gateway Timeout", message: "Request timed out while fetching product data", timestamp: new Date().toISOString() }); } else { res.status(500).json({ error: "Internal Server Error", message: "An unexpected error occurred", timestamp: new Date().toISOString() }); } } /** * Set the Widget endpoint. * * @param req The request object. * @param res The response object. */ setWidgetEndpoint(req, res) { try { res.type('application/javascript'); res.sendFile(path_1.default.join(AmwApi.PROJECT_DIR, 'dist', 'widgets', 'widget.js')); } catch (error) { this.handleError(error, req, res); } } /** * Find product in cache. * * @param key The key to find in the cache. * @returns The cached product if found, null otherwise. */ findInCache(key) { return __awaiter(this, void 0, void 0, function* () { if (!this.cache) { return null; } try { const cachedData = yield this.cache.get(key); if (cachedData) { this.log.info(`Product found in cache: ${key}`); return JSON.parse(cachedData); } this.log.info(`Product not found in cache: ${key}`); return null; } catch (error) { this.log.error(`Error accessing cache for key ${key}:`, error); return null; } }); } /** * Save the product in the cache. * * @param key The key to save in the cache. * @param product The product to save. */ saveInCache(key, product) { return __awaiter(this, void 0, void 0, function* () { if (!this.cache) { return; } try { this.log.info(`Saving product in cache: ${key}`); yield this.cache.set(key, JSON.stringify(product), 'EX', this.ttl); } catch (error) { this.log.error(`Error saving to cache for key ${key}:`, error); // Don't throw error, caching failure shouldn't break the request } }); } } exports.AmwApi = AmwApi; // Static attributes AmwApi.PROJECT_DIR = config_1.default.get('Server.projectDir'); AmwApi.CACHE_KEY_PREFIX = 'amw:product:'; AmwApi.MAX_KEYWORD_LENGTH = 100; AmwApi.MAX_ID_LENGTH = 50; AmwApi.REQUEST_TIMEOUT = 30000; // 30 seconds //# sourceMappingURL=AmwApi.js.map